为什么要在 Java 中的方法参数上使用关键字“final”?

Why should I use the keyword "final" on a method parameter in Java?

提问人: 提问时间:2/1/2009 最后编辑:Basil Bourque 更新时间:11/22/2023 访问量:176961

问:

我不明白当关键字用于方法参数时,它在哪里真正方便。final

如果我们排除匿名类的使用、可读性和意图声明,那么它对我来说似乎几乎毫无价值。

强制执行某些数据保持不变并不像看起来那么强大。

  • 如果参数是基元,则它不会起作用,因为参数是作为值传递给方法的,并且更改它不会在范围之外产生任何影响。

  • 如果我们通过引用传递参数,那么引用本身就是一个局部变量,如果引用是从方法内部更改的,则在方法范围之外不会产生任何影响。

考虑下面的简单测试示例。 该测试通过,尽管该方法更改了给定给它的引用的值,但它没有效果。

public void testNullify() {
    Collection<Integer> c  = new ArrayList<Integer>();      
    nullify(c);
    assertNotNull(c);       
    final Collection<Integer> c1 = c;
    assertTrue(c1.equals(c));
    change(c);
    assertTrue(c1.equals(c));
}

private void change(Collection<Integer> c) {
    c = new ArrayList<Integer>();
}

public void nullify(Collection<?> t) {
    t = null;
}
Java 引用 最终 按值传递

评论

118赞 Jon Skeet 2/1/2009
关于术语的快速点 - Java 根本没有通过引用。它通过传递引用,这不是一回事。使用真正的引用传递语义,代码的结果会有所不同。
10赞 NobleUplift 10/14/2013
“按引用传递”和“按值传递引用”有什么区别?
1赞 jerslan 10/16/2013
在 C 上下文中描述这种差异更容易(至少对我来说是这样)。如果我传递一个指向类似 <code>int foo(int bar)</code> 的方法的指针,则该指针是按值传递的。这意味着它被复制了,所以如果我在该方法中做一些事情,比如 <code>free(bar);bar = malloc(...);</代码>然后我刚刚做了一件非常糟糕的事情。免费调用实际上会释放所指向的内存块(因此,无论我传入什么指针,现在都悬空)。但是,<code>int foo(int &bar)</bar> 表示代码有效,传入的指针的值将更改。
4赞 jerslan 10/16/2013
第一个应该是,最后一个.后者通过引用传递指针,前者通过值传递引用。int foo(int* bar)int foo(int* &bar)
2赞 Victor Zamanian 6/15/2016
@Martin,在我看来,这是一个很好的问题;请参阅问题的标题和帖子内容,以解释为什么提出问题。也许我误解了这里的规则,但这正是我在搜索“方法中使用最终参数”时想要的问题。

答:

131赞 Jon Skeet 2/1/2009 #1

是的,不包括匿名类、可读性和意图声明,它几乎毫无价值。这三样东西一文不值吗?

就我个人而言,我倾向于不用于局部变量和参数,除非我在匿名内部类中使用变量,但我当然可以看到那些想要明确参数值本身不会改变的人的观点(即使它引用的对象改变了它的内容)。对于那些发现这增加了可读性的人来说,我认为这是一件完全合理的事情。final

如果有人真的声称它确实以某种方式保持数据不变,那么你的观点会更重要 - 但我不记得看到过任何这样的说法。你是说有相当多的开发人员认为它比实际效果更好吗?final

编辑:我真的应该用 Monty Python 参考来总结所有这些;这个问题似乎有点类似于问“罗马人曾经为我们做过什么?

评论

19赞 James Schek 2/5/2009
但用他的丹麦语套用 Krusty 的话来说,他们最近为我们做了什么?=)
0赞 gonzobrains 8/3/2012
尤瓦尔。这很有趣!我想和平即使被剑刃强制执行也会发生!
2赞 NobleUplift 10/14/2013
这个问题似乎更像是问“罗马人没有为我们做了什么?”,因为它更像是对最后一个关键词没有做什么的批评。
1赞 Stuart Rossiter 11/11/2015
“你是说有相当多的开发人员认为 final 的效果比实际效果更大吗?”对我来说,这是主要问题:我强烈怀疑使用它的很大一部分开发人员认为它强制执行调用者传递的项的不变性,而事实并非如此。当然,人们随后会卷入关于编码标准是否应该“防止”概念误解(“称职”的开发人员应该意识到这一点)的争论中(然后这就会走向一个超出范围的意见类型的问题)!
1赞 Jon Skeet 9/18/2017
@SarthakMittal:除非你实际使用它,否则该值不会被复制,如果你想知道的话。
20赞 Germán 2/1/2009 #2

在方法参数中使用 final 与调用方端的参数发生的情况无关。它只是为了将其标记为在该方法中不更改。当我尝试采用一种更实用的编程风格时,我看到了它的价值。

评论

3赞 Beni Cherniavsky-Paskin 3/22/2015
确切地说,它不是函数接口的一部分,只是实现的一部分。令人困惑的是,java 允许(但 dirsregards)接口/抽象方法声明中的参数。final
9赞 starblue 2/1/2009 #3

就我个人而言,我不在方法参数上使用 final,因为它会给参数列表增加太多混乱。 我更愿意强制执行方法参数不会通过 Checkstyle 之类的东西来更改。

对于局部变量,我尽可能使用 final,我甚至让 Eclipse 在我的个人项目设置中自动执行此操作。

我当然想要像 C/C++ const 这样更强大的东西。

评论

0赞 Darrell Teague 12/6/2016
不确定 IDE 和工具参考是否适用于 OP 发布或主题。即,“final”是编译时检查引用未被更改/抢劫。此外,要真正执行这些事情,请参阅关于最终引用的子成员不保护的答案。例如,在构建 API 时,使用 IDE 或工具不会帮助外部各方使用/扩展此类代码。
80赞 Michael Borgwardt 2/1/2009 #4

让我解释一下你必须使用 final 的一种情况,Jon 已经提到过:

如果在方法中创建一个匿名内部类,并在该类中使用局部变量(如方法参数),则编译器会强制您将参数设为 final:

public Iterator<Integer> createIntegerIterator(final int from, final int to)
{
    return new Iterator<Integer>(){
        int index = from;
        public Integer next()
        {
            return index++;
        }
        public boolean hasNext()
        {
            return index <= to;
        }
        // remove method omitted
    };
}

这里的 and 参数必须是最终的,以便它们可以在匿名类中使用。fromto

提出该要求的原因是:局部变量位于堆栈上,因此它们仅在执行方法时存在。但是,匿名类实例是从该方法返回的,因此它可能会存在更长的时间。您无法保留堆栈,因为后续方法调用需要它。

因此,Java 所做的是将这些局部变量的副本作为隐藏实例变量放入匿名类中(如果您检查字节码,您可以看到它们)。但是,如果它们不是最终的,人们可能会期望匿名类和方法看到另一个人对变量所做的更改。为了保持只有一个变量而不是两个副本的错觉,它必须是最终的。

评论

1赞 hhafez 2/2/2009
你把我从“但如果它们不是最终的......”中迷失了。你能试着改写一下吗,也许我没有足够的咖啡。
1赞 Michael Borgwardt 2/2/2009
你有一个局部变量 from - 问题是如果你在方法中使用 anon 类实例并更改 from 的值会发生什么 - 人们会期望更改在方法中可见,因为他们只看到一个变量。为了避免这种混淆,它必须是最终的。
0赞 vickirk 2/10/2010
它不会复制,它只是对所引用的任何对象的引用。
1赞 Michael Borgwardt 2/10/2010
@vickirk:确保它复制 - 在引用类型的情况下,引用。
0赞 Pacerier 11/17/2011
顺便说一句,假设我们没有引用这些变量的匿名类,您是否知道 HotSpot 眼中的函数参数和非最终函数参数之间是否有任何区别?final
28赞 Fortyrunner 2/1/2009 #5

我一直在参数上使用 final。

它增加了那么多吗?没有。

我会把它关掉吗?不。

原因是:我发现了 3 个错误,人们编写了草率的代码并且未能在访问器中设置成员变量。事实证明,所有错误都很难找到。

我希望看到它在未来版本的 Java 中成为默认值。传递值/引用的事情绊倒了很多初级程序员。

还有一件事..我的方法往往具有较少数量的参数,因此方法声明中的额外文本不是问题。

评论

4赞 Jeff Axelrod 5/12/2011
我也打算建议这一点,最终是未来版本中的默认值,并且您必须指定构思的“可变”或更好的关键字。这是一篇关于这个的好文章: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
0赞 user949300 9/27/2017
已经很久了,但你能提供一个你抓到的错误的例子吗?
0赞 Fortyrunner 9/28/2017
查看得票最多的答案。这有一个很好的例子,其中没有设置成员变量,而是参数发生了变异。
239赞 Jerry Sha 2/2/2009 #6

有时,明确表示变量不会改变(为了可读性)是件好事。这里有一个简单的例子,使用可以省去一些可能的麻烦:final

public void setTest(String test) {
    test = test;
}

如果您忘记了 setter 上的“this”关键字,则不会设置要设置的变量。但是,如果在参数上使用了关键字,则会在编译时捕获 bug。final

评论

70赞 AvrDragon 4/5/2012
顺便说一句,无论如何,您都会看到警告“对变量测试的赋值没有效果”
14赞 Sumit Desai 4/30/2013
@AvrDragon 但是,我们也可能忽略这个警告。因此,最好有一些东西可以阻止我们走得更远,例如编译错误,我们将通过使用最终关键字来获得它。
11赞 arkon 5/28/2013
@AvrDragon 这取决于开发环境。无论如何,您都不应该依赖 IDE 来为您捕获此类内容,除非您想养成坏习惯。
26赞 AvrDragon 5/28/2013
@b1naryatr0phy实际上这是一个编译器警告,而不仅仅是 IDE 提示
10赞 Stuart Rossiter 11/11/2015
@SumitDesai “但是,我们也可能忽略这个警告。因此,最好有一些东西可以阻止我们走得更远,比如编译错误,我们将通过使用最后一个关键字来获得它。我同意你的观点,但我认为这是一个非常强烈的声明,我认为许多 Java 开发人员会不同意。编译器警告的存在是有原因的,一个称职的开发人员不应该需要错误来“强迫”他们考虑其影响。
0赞 Michael Rutherfurd 2/2/2009 #7

将 final 添加到参数声明的另一个原因是,它有助于识别需要重命名的变量,作为“提取方法”重构的一部分。我发现,在开始大型方法重构之前,将 final 添加到每个参数中会快速告诉我在继续之前是否需要解决任何问题。

但是,我通常会在重构结束时将它们删除为多余的。

338赞 Basil Bourque 4/30/2012 #8

停止变量的重新赋值

虽然这些答案在智力上很有趣,但我还没有读过简短的简单答案:

当您希望编译器阻止 变量被重新分配给其他对象。

无论变量是静态变量、成员变量、局部变量还是参数/参数变量,效果都是完全相同的。

让我们看看效果如何。

考虑这个简单的方法,其中两个变量(argx)都可以重新分配不同的对象。

// Example use of this method: 
//   this.doSomething( "tiger" );
void doSomething( String arg ) {
  String x = arg;   // Both variables now point to the same String object.
  x = "elephant";   // This variable now points to a different String object.
  arg = "giraffe";  // Ditto. Now neither variable points to the original passed String.
}

将局部变量标记为 final。这会导致编译器错误。

void doSomething( String arg ) {
  final String x = arg;  // Mark variable as 'final'.
  x = "elephant";  // Compiler error: The final local variable x cannot be assigned. 
  arg = "giraffe";  
}

相反,让我们将参数变量标记为 final。这也会导致编译器错误。

void doSomething( final String arg ) {  // Mark argument as 'final'.
  String x = arg;   
  x = "elephant"; 
  arg = "giraffe";  // Compiler error: The passed argument variable arg cannot be re-assigned to another object.
}

故事的寓意:

如果要确保变量始终指向同一对象, 将变量标记为 final

从不重新分配参数

作为良好的编程实践(在任何语言中),切勿将参数/参数变量重新分配给调用方法传递的对象以外的对象。在上面的例子中,永远不应该写 .既然人类会犯错误,而程序员也是人,那就请编译器来协助我们吧。将每个参数/参数变量标记为“final”,以便编译器可以查找并标记任何此类重新赋值。arg =

回顾过去

正如其他答案中所指出的... 鉴于 Java 最初的设计目标是帮助程序员避免愚蠢的错误,例如读取数组的末尾,Java 应该被设计为自动将所有参数/参数变量强制执行为“final”。换句话说,参数不应该是变量。但事后诸葛亮是 20/20 的愿景,Java 设计人员当时忙得不可开交。

那么,总是添加到所有参数中?final

我们是否应该向每个声明的方法参数添加内容?final

  • 从理论上讲,是的。
  • 在实践中,没有。
    ➥ 仅当方法的代码很长或很复杂时才添加,其中参数可能会被误认为是局部变量或成员变量,并可能被重新分配。
    final

如果你接受从不重新分配一个参数的做法,你会倾向于在每个参数上添加一个。但这很乏味,使声明更难阅读。final

对于参数显然是参数,而不是局部变量或成员变量的简短简单代码,我不会费心添加 .如果代码非常明显,我或任何其他程序员在进行维护或重构时都不会意外地将参数变量误认为参数以外的东西,那么请不要打扰。在我自己的工作中,我只添加更长或更复杂的代码,其中参数可能会被误认为是局部变量或成员变量。finalfinal

为完整性而添加 #Another 案例

public class MyClass {
    private int x;
    //getters and setters
}

void doSomething( final MyClass arg ) {  // Mark argument as 'final'.
  
   arg =  new MyClass();  // Compiler error: The passed argument variable arg  cannot be re-assigned to another object.

   arg.setX(20); // allowed
  // We can re-assign properties of argument which is marked as final
 }

record

Java 16 带来了新的记录功能。记录是定义类的一种非常简短的方法,其中心目的是仅以不可变和透明的方式携带数据。

您只需声明类名及其成员字段的名称和类型即可。编译器隐式提供构造函数 getter 和 。equalshashCodetoString

这些字段是只读的,没有 setter。所以 a 是一种无需标记参数的情况。它们实际上已经是最终的。事实上,编译器在声明记录的字段时禁止使用。recordfinalfinal

public record Employee( String name , LocalDate whenHired )  // 🡄 Marking `final` here is *not* allowed.
{
}

如果提供可选构造函数,则可以将 标记 .final

public record Employee(String name , LocalDate whenHired)  // 🡄 Marking `final` here is *not* allowed.
{
    public Employee ( final String name , final LocalDate whenHired )  // 🡄 Marking `final` here *is* allowed.
    {
        this.name = name;
        whenHired = LocalDate.MIN;  // 🡄 Compiler error, because of `final`. 
        this.whenHired = whenHired;
    }
}

评论

31赞 Stijn de Witt 11/27/2013
“作为良好的编程实践(在任何语言中),你永远不应该重新分配参数/参数变量 [..]”对不起,我真的必须把你叫出来。重新分配参数是 Javascript 等语言的标准做法,其中传递的参数数量(或即使有任何传递)不是由方法签名决定的。例如,给定一个签名,例如:“function say(msg)”,人们会确保分配参数“msg”,如下所示:“msg = msg || '你好,世界!世界上最好的 Javascript 程序员正在打破你的良好做法。只需阅读 jQuery 源代码即可。
60赞 Basil Bourque 11/28/2013
@StijndeWitt 您的示例显示了重新分配参数变量的问题。您丢失了信息,但没有任何回报:(a) 您丢失了传递的原始值,(b) 您丢失了调用方法的意图(调用者传递了“Hello World!”还是我们默认了)。a 和 b 对于测试、长代码以及以后进一步更改值都很有用。我坚持我的说法:arg vars 永远不应该被重新分配。您的代码应为:。根本没有理由使用单独的变量。唯一的成本是几个字节的内存。message = ( msg || 'Hello World"' )
10赞 Stijn de Witt 4/10/2014
@Basil:更多的代码(以字节为单位)和 Javascript 确实很重要。严重。与许多事情一样,它是基于意见的。完全可以完全忽略这种编程实践,仍然编写出色的代码。一个人的编程实践并不能使它成为每个人的实践。坚持下去,无论如何,我选择写不同的。这会让我成为一个糟糕的程序员,还是我的代码是糟糕的代码?
16赞 Beni Cherniavsky-Paskin 3/22/2015
使用可能会让我以后不小心使用 .当我想要的合约是“具有 no/null/undefined arg 的行为是不可分割的传递”时,在函数的早期提交它是一个很好的编程实践。[这可以通过从 开始而不重新分配 ,但对于多个参数来说,这变得笨拙。在极少数情况下,函数中的逻辑应该关心是否使用了默认值,我宁愿指定一个特殊的哨兵值(最好是 public)。message = ( msg || 'Hello World"' )msg"Hello World"if (!msg) return myfunc("Hello World");
8赞 Nir Alfasi 5/14/2016
@BeniCherniavsky-Paskin,你所描述的风险只是因为 和 之间的相似性。但是,如果他称它为类似的东西或其他提供额外背景的东西 - 出错的可能性要低得多。专注于他说的话,而不是他“如何”说。;)messagemsgprocessedMsg
1赞 Chris Milburn 1/8/2013 #9

我从不在参数列表中使用 final,它只会像以前的受访者所说的那样增加混乱。同样在 Eclipse 中,您可以设置参数赋值以生成错误,因此在参数列表中使用 final 对我来说似乎是多余的。 有趣的是,当我启用参数分配的 Eclipse 设置时,在它上面生成错误时,捕获了此代码(这只是我记得流程的方式,而不是实际代码。

private String getString(String A, int i, String B, String C)
{
    if (i > 0)
        A += B;

    if (i > 100)
        A += C;

    return A;
}

扮演魔鬼的代言人,这样做到底有什么错?

评论

0赞 Darrell Teague 8/14/2018
要谨慎地将 IDE 与运行时 JVM 区分开来。当编译的字节码在服务器上运行时,IDE 所做的任何事情都是无关紧要的,除非 IDE 添加了代码来防止成员变量抢劫,例如代码中的缺陷,当打算不重新分配变量但错误地重新分配时,因此 final 关键字的目的。
4赞 Lawrence 10/29/2013 #10

由于 Java 传递了参数的副本,我觉得它的相关性相当有限。我想这个习惯来自C++时代,在这个时代,你可以通过执行 .我觉得这种东西会让你相信开发人员天生就很愚蠢,需要保护他免受他输入的每个角色的影响。谦虚地说,即使我省略了,我写的错误也很少(除非我不希望有人覆盖我的方法和类)。也许我只是一个老派的开发人员。finalconst char const *final

-1赞 KunYu Tsai 3/18/2016 #11

跟进米歇尔的帖子。我给自己做了另一个例子来解释它。我希望它能有所帮助。

public static void main(String[] args){
    MyParam myParam = thisIsWhy(new MyObj());
    myParam.setArgNewName();

    System.out.println(myParam.showObjName());
}

public static MyParam thisIsWhy(final MyObj obj){
    MyParam myParam = new MyParam() {
        @Override
        public void setArgNewName() {
            obj.name = "afterSet";
        }

        @Override
        public String showObjName(){
            return obj.name;
        }
    };

    return myParam;
}

public static class MyObj{
    String name = "beforeSet";
    public MyObj() {
    }
}

public abstract static class MyParam{
    public abstract void setArgNewName();
    public abstract String showObjName();
}

从上面的代码中,在 thisIsWhy() 方法中,我们实际上没有将 [参数 MyObj obj] 分配给 MyParam的真实引用。相反,我们只是在 MyParam 中的方法中使用 [argument MyObj obj]。

但是在我们完成thisIsWhy()方法之后,argument(object)MyObj是否仍然存在?

似乎应该,因为我们可以看到,我们仍然调用方法 showObjName() 并且它需要到达 obj。MyParam 仍将使用/到达方法参数,即使已经返回的方法!

Java 真正实现这一点的方式是生成一个副本,也是 MyParam 对象中参数 MyObj obj 的隐藏引用(但它不是 MyParam 中的正式字段,因此我们无法看到它)

当我们调用“showObjName”时,它将使用该引用来获取相应的值。

但是,如果我们没有将参数放在最终状态,这会导致一种情况,我们可以将新的内存(对象)重新分配给参数 MyObj obj

从技术上讲,根本没有冲突!如果我们被允许这样做,情况如下:

  1. 我们现在有一个隐藏的 [MyObj obj] 指向现在位于 MyParam 对象中的 [堆中的内存 A]。
  2. 我们还有另一个 [MyObj obj],它是 [堆中的内存 B] 的参数点,现在存在于 thisIsWhy 方法中。

没有冲突,但“令人困惑!!因为它们都使用相同的“引用名称”,即“obj”。

为避免这种情况,请将其设置为“final”,以避免程序员执行“容易出错”的代码。

3赞 Darrell Teague 6/21/2016 #12

简短的回答:有一点帮助,但是......请改用客户端的防御性编程。final

事实上,问题在于它只强制执行引用是不变的,兴高采烈地允许引用的对象成员在调用者不知情的情况下发生变异。因此,这方面的最佳实践是在调用方进行防御性编程,创建深度不可变的实例或对象的深度副本,这些对象有被不道德的 API 抢劫的危险。final

评论

2赞 Madbreaks 4/11/2018
“final 的问题在于它只强制执行引用是不变的”——不真实,Java 本身阻止了这一点。传递给方法的变量不能通过该方法更改其引用。
0赞 Darrell Teague 4/12/2018
请在发布前进行研究...stackoverflow.com/questions/40480/......
0赞 Darrell Teague 4/12/2018
简单地说,如果对参考文献的引用确实不能改变,那么就不会讨论防御性复制、不变性、不需要最终关键字等。
0赞 Madbreaks 4/13/2018
要么你误会了我,要么你弄错了。如果我将对象引用传递给某个方法,并且该方法重新分配它,则当该方法完成其执行时,原始引用对调用方(我)保持不变。Java 是严格按值传递的。你厚颜无耻地断言我没有做过任何研究。
0赞 Madbreaks 4/13/2018
投反对票,因为 op 问为什么使用 final,而你只给出了一个不正确的理由。