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

455

在方法参数上使用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;
}

118
关于术语的一个要点 - Java根本没有传递引用。它采用传递值的方式来传递引用,这并不是同一件事情。如果真正采用传递引用的语义,你的代码结果将会有所不同。 - Jon Skeet
10
传递引用和传值引用的区别是什么? - NobleUplift
1
在C语言的背景下更容易描述这种差异(至少对我来说是这样)。如果我传递一个指向方法的指针,如:<code>int foo(int bar)</code>,那么该指针会被按值传递。这意味着它被复制了,因此,如果我在该方法中执行类似于<code>free(bar); bar = malloc(...);</code>的操作,那么我刚刚做了一件很糟糕的事情。free调用实际上将释放指针所指向的内存块(因此传入的任何指针现在都是悬空的)。但是,<code>int foo(int &bar)</code>表示该代码是有效的,并且传入的指针的值将被更改。 - jerslan
4
第一个应该是int foo(int* bar),最后一个是int foo(int* &bar)。后者是通过引用传递指针,前者是通过值传递引用。 - jerslan
2
@Martin,我认为这是一个好的问题;请看问题的标题和帖子内容,以了解提出这个问题的原因。也许我在这里误解了规则,但这正是我搜索“方法中final参数的用途”时想要的问题。 - Victor Zamanian
标题是一个问题,但正文并不是。这是一个基于误解的例子。在另一个作用域中更改引用(通过方法调用)不会更改当前作用域中的引用。 - Martin
12个回答

338

停止变量的重新赋值

虽然这些答案在理论上很有趣,但我还没有读到简短的简单答案:

当你希望编译器阻止变量被重新赋值为不同的对象时,使用关键字final

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

示例

让我们看看实际效果。

考虑这个简单的方法,在这个方法中,两个变量(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”。
永远不要重新分配参数
作为良好的编程实践(无论是哪种语言),你永远不应该将参数/参数变量重新分配给除调用方法传递的对象之外的其他对象。在上面的例子中,永远不应该写下这行代码“arg =”。由于人会犯错误,而程序员也是人,让我们请求编译器来帮助我们。将每个参数/参数变量标记为“final”,以便编译器可以找到并标记任何此类重新分配。
回顾一下
正如其他答案中所指出的... 鉴于Java最初的设计目标是帮助程序员避免愚蠢的错误,例如读取数组末尾之后的内容,Java本应自动将所有参数/参数变量强制为“final”。换句话说,参数不应该是变量。但是事后诸葛亮,当时的Java设计师手头已经很忙了。
所以,每个参数都要添加final吗?
我们应该将final添加到每个声明的方法参数吗?
理论上是的。
实践中不是这样的。只有当方法的代码很长或复杂时,参数可能会被误认为是局部变量或成员变量并重新赋值时,才需要添加final。
如果你坚持不重新分配参数的做法,你会倾向于为每个参数添加final。但这很繁琐,也使声明变得更难阅读。
对于简短的简单代码,参数显然是一个参数,而不是局部变量或成员变量,我不会费心添加final。如果代码非常明显,没有机会让我或其他程序员在维护或重构时错误地将参数变量误认为是其他东西,那就不必费心了。在我的工作中,我只在更长或更复杂的代码中添加final,以防参数被误认为局部变量或成员变量。
#为了完整性添加的另一种情况
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
 }

记录

Java 16引入了新的记录功能。记录是一种非常简洁的方式来定义一个类,其主要目的是仅仅用于携带数据,以不可变和透明的方式。

您只需声明类名以及其成员字段的名称和类型。编译器隐式提供构造函数、getter方法、equalshashCode方法以及toString方法。

这些字段是只读的,没有setter方法。因此,记录是一种情况,不需要将参数标记为final。它们已经是有效的final。实际上,编译器禁止在声明记录的字段时使用final关键字。

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
作为良好的编程实践(不论语言),你永远不应该重新分配参数/参数变量[..]。抱歉,我必须在这个问题上指出您的错误。在像JavaScript这样的语言中,重新分配参数是标准做法,因为传递的参数数量(甚至是否传递)并不由方法签名指定。例如,对于类似"function say(msg)"这样的签名,人们将确保分配参数'msg',如下所示:"msg = msg || 'Hello World!';"。世界上最好的JavaScript程序员正在打破你的良好实践。只要看一下jQuery源代码就知道了。 - Stijn de Witt
60
您的示例展示了重新分配参数变量的问题,您失去了信息,却没有获得任何好处:(a) 您失去了传递的原始值, (b) 您失去了调用方法的意图(调用者是否传递了“你好世界!”或我们使用默认值)。在测试、长代码和以后进一步更改值时,a和b都是有用的。我坚持我的观点:参数变量不应该被重新赋值。您的代码应为:message = ( msg || 'Hello World"' )。没有理由不使用单独的变量。唯一的代价就是几个字节的内存。 - Basil Bourque
10
@Basil: 这需要更多的代码(以字节为单位),并且在JavaScript中,这很重要。与许多事情一样,这是基于个人观点的。完全可以完全忽略此编程实践,仍然编写出色的代码。一个人的编程实践并不能成为所有人的实践。无论你坚持如何,我选择以不同的方式编写它。这是否使我成为一个糟糕的程序员,或者我的代码是糟糕的代码? - Stijn de Witt
16
使用 message = ( msg || 'Hello World"' ) 的风险是我以后会意外使用 msg。当我想要的合约是“对于没有/空/未定义参数的行为与传递 "Hello World" 的行为无法区分”,在函数早期就承诺它是一个良好的编程实践。可以通过不重新赋值来实现这一点,例如从 if (!msg) return myfunc("Hello World"); 开始,但是在多个参数的情况下这样做会变得笨重。在极少数需要函数逻辑关心是否使用了默认值的情况下,我宁愿指定一个特殊的标记值(最好是公共的)。 - Beni Cherniavsky-Paskin
8
你描述的风险仅因“message”和“msg”之间的相似性而产生。但如果他将其命名为类似于“processedMsg”或其他提供额外上下文的名称,则出错的机会就会降低许多。聚焦于他说的内容,而不是“他如何表达”。 ;) - Nir Alfasi
显示剩余19条评论

239

有时候明确(为了可读性)表达变量不会改变是很好的。以下是一个简单的例子,其中使用 final 可以避免一些可能的麻烦:

public void setTest(String test) {
    test = test;
}
如果在setter方法上忘记了使用'this'关键字,那么你想要设置的变量将无法被设置。不过,如果在参数上使用了final关键字,那么这个错误会在编译时被捕获到。

70
顺便提醒一下,您会看到警告信息“变量test的赋值没有效果”,无论如何。 - AvrDragon
14
但是,我们也可能会忽略警告。因此,最好有一些东西阻止我们继续进行,比如使用final关键字可以得到编译错误。 - Sumit Desai
11
这取决于开发环境。无论如何,你都不应该依赖IDE来捕捉这样的问题,除非你想养成不好的习惯。 - arkon
26
实际上,这是编译器警告,而不仅仅是IDE提示。 - AvrDragon
10
“但是,我们也许会忽略警告。因此,最好有一些可以阻止我们继续前进的东西,比如使用final关键字,我们将得到编译错误。”我理解您的观点,但这是一个非常强烈的声明,我认为很多Java开发人员会不同意。编译器警告是有原因的,有能力的开发人员不应需要错误来“强制”他们考虑其含义。 - Stuart Rossiter
显示剩余4条评论

131

除去匿名类、可读性和意图声明,它几乎毫无用处。但这三个因素是毫无价值的吗?

就我个人而言,除非我在使用变量进行匿名内部类,否则我不倾向于对局部变量和参数使用 final。但我可以理解那些希望清楚表明参数值本身不会改变(即使它所指向的对象改变其内容)的人们的观点。对于那些认为这有助于可读性的人,我认为这完全是合理的。

如果有人确实声称 final 以某种方式保持数据不变,而实际上并没有达到这个效果,那么您的观点将更为重要 - 但我记不起来曾经看到过任何这样的说法。你是在暗示有大量的开发者认为 final 的效果比它实际上的效果更强吗?

编辑:我真的应该用蒙提·派森的话来总结这一切;这个问题似乎有点像问“罗马人为我们做了什么?”


19
但换句话说,用克拉斯蒂与他的丹麦面包来解释,他们最近为我们做了什么呢?=) - James Schek
2
这个问题更像是在问,“罗马人为我们做了什么?”因为它更多地是对final关键字所不能做的事情进行批评。 - NobleUplift
1
你是在暗示有一大批开发者认为final的影响比它实际上要大吗?对我来说,这才是主要问题:我强烈怀疑使用final的开发人员中有很大一部分认为它会执行调用方传递项的不可变性,而实际上并非如此。当然,这就让我们卷入了一个辩论,即编码标准是否应该“防范”概念上的误解(一个“称职”的开发人员应该意识到)还是不应该(这就走向了一个超出SO范畴的观点类型问题)! - Stuart Rossiter
@Jon Skeet:假设我有一个声明为final的方法参数,以及一个内部类在该方法内部,但我没有在我的内部类中使用那个final参数,那么我的final引用是否可以进行垃圾回收? - Sarthak Mittal
1
@SarthakMittal:如果你想知道的话,除非你实际使用它,否则该值不会被复制。 - Jon Skeet
显示剩余3条评论

80

让我稍微解释一下,有一种情况下你必须使用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
    };
}

这里需要将fromto参数声明为final,这样它们才能在匿名类中使用。

这个要求的原因是:局部变量存储在栈上,因此它们只存在于方法执行期间。但是,匿名类实例是从方法返回的,因此它可能存在更长的时间。你无法保留堆栈,因为它需要用于后续的方法调用。

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


1
你从“但如果它们不是最终版…”开始就把我弄糊涂了。你能否试着重新表达一下?也许是因为我还没喝够咖啡。 - hhafez
1
你有一个来自本地的变量 - 问题是如果你在方法内使用匿名类实例并且它改变了来自的值 - 人们会期望更改在方法中可见,因为他们只看到一个变量。为了避免这种混淆,必须将其声明为final。 - Michael Borgwardt
1
@vickirk:当涉及到引用类型时,它确实会创建一个引用的副本。 - Michael Borgwardt
@MichaelBorgwardt 好的,明白了。 - Sumit Desai
这就是为什么Java没有真正的闭包。 - Wei Qiu
显示剩余5条评论

28

我经常在参数上使用final。

它增加了很多吗?实际上并没有。

我会关闭它吗?不会。

原因是:我发现3个错误,人们编写了松散的代码并未在访问器中设置成员变量。所有这些错误都很难找到。

我希望在Java的将来版本中把这个作为默认选项。传值/引用的问题使很多初级程序员困惑。

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


4
我也想建议这一点,即在将来版本中将final设为默认值,并且你必须指定“mutable”或更好的关键字。 这里有一篇关于此的好文章:http://lpar.ath0.com/2008/08/26/java-annoyance-final-parameters/ - Jeff Axelrod
很久没联系了,你能提供一下你发现的那个 bug 的例子吗? - user949300
查看得票最多的答案。其中有一个非常好的例子,成员变量没有被设置,而是改变了参数。 - Fortyrunner

20

在方法参数中使用 final 关键字与调用方的参数发生的事情无关,它只是标记该参数在该方法内部不会被改变。随着我尝试采用更加函数式的编程风格,我对此有些认可。


3
没错,这不是函数接口的一部分,只属于实现部分。Java 在接口/抽象方法声明中允许但忽略参数上的 final 关键字有些令人困惑。参考链接:https://dev59.com/72435IYBdhLWcg3wfgMf - Beni Cherniavsky-Paskin

9

个人而言,我不会在方法参数上使用final,因为它会给参数列表增加太多杂乱的内容。我更喜欢通过类似Checkstyle的方式来强制执行方法参数不被更改。

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

我肯定希望有像C/C++ const这样更强大的东西。


不确定IDE和工具引用是否适用于OP发布或主题。即“final”是一个编译时检查,确保引用未被更改/篡改。此外,要真正强制执行这些内容,请参见有关最终引用的子成员无保护的答案。例如,在构建API时,使用IDE或工具将无法帮助外部方使用/扩展此类代码。 - Darrell Teague

4
自从Java传递参数的是副本,我感觉使用final的相关性相当有限。我猜这种习惯来自于C++时代,在那里你可以通过使用const char const *来禁止引用内容被更改。我认为这种东西让你相信开发者天生就是愚蠢的,并且需要保护他免受每个字符的影响。恕我直言,即使我省略了final(除非我不希望别人重写我的方法和类),我也很少写错代码。也许我只是一个老派的开发者。

3
简短回答:使用final确实有些帮助,但是……在客户端上使用防御性编程更好。
事实上,final存在的问题在于它只强制要求引用不被改变,并且欣然接受被引用对象成员的变异,而调用者却不知情。因此,在这方面最佳实践是在调用者端进行防御性编程,创建深度不可变的实例或对象的深拷贝,以防止被不道德的API所攻击。

2
“final的问题在于它只强制引用不变。这是不正确的,Java本身就可以防止这种情况发生。传递到方法中的变量不能被该方法更改其引用。” - Madbreaks
请在发布之前进行研究... https://dev59.com/EXVD5IYBdhLWcg3wQJOT - Darrell Teague
简单来说,如果引用不能被更改的说法是正确的,那么就不会有关于防御性复制、不可变性、final 关键字等方面的讨论。 - Darrell Teague
要么你误解了我的意思,要么你错了。如果我将一个对象引用传递给一个方法,并且该方法重新分配它,当方法完成执行时,原始引用对于调用者仍然保持不变。Java 严格按值传递。而你大胆地假定我没有做过任何研究。 - Madbreaks
因为楼主问了为什么要使用final,而你只给出了一个错误的原因,所以我要给你点个踩。 - Madbreaks
事实摆在那里...关于传递的引用是按值传递的一些细微语义问题,但是引用(指针-我也使用“C”编程)...可以像提供的示例所示指向任何东西。这导致返回的值被更改。至于OP的“问题”...早期的评论中写道“这不是一个问题,而是你观点的陈述”...因此,对于是否存在问题而言,存在一些分歧,而不是关于使用final关键字的相当模糊的讨论。 - Darrell Teague

1
我从不在参数列表中使用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;
}

从反面角度来看,这样做有什么问题吗?


要注意区分IDE和运行时JVM。除非IDE添加了代码以防止成员变量被篡改(例如在编写代码时意图不应重新分配变量但错误地重新分配变量),否则IDE所做的任何事情都与在服务器上运行已编译的字节码无关 - 因此final关键字的目的就在于此。 - Darrell Teague

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接