在循环中声明本地变量为final

4

我知道已经有类似的问题被提出并回答了,我阅读了我能找到的那些问题,但仍不完全清楚。

考虑以下代码片段:

public static void fooMethod {

   while(<...>) {
     ....
     final int temp = <something>;
     ....
   }
}

没有内部类,没有其他特殊或不寻常的东西。这对我来说似乎是违反直觉的。
在上面的示例中声明局部变量为final是否有任何作用?
我是否正确地理解,即无论是否使用final,编译器都会生成完全相同的字节码?
我在这里错过了什么吗?如果是RTFM案例,请指点我正确的方向。
跟进问题(如果我可以)
通过像这样重新编写(理解temp不必是原始的),我获得和/或失去了什么?
public static void fooMethod2 {

   int temp;
   while(<...>) {
     ....
     temp = <something>;
     ....
   }
}

1
大概它的作用和其他地方一样:当你尝试修改变量时,编译器会阻止你。 - Cairnarvon
这个变量是非常局部的(仅限于循环体内)。它甚至在同一个循环中也没有被引用。它通过禁止可能的不同目的的重用来保护可读性吗? - PM 77-1
3个回答

9
简述:当在局部变量和参数中使用时,final关键字不会出现在生成的字节码(.class文件)中,并且如预期的那样,在运行时不起作用。(尽管在编译时可能会有所不同,请参见下文。)在这些情况下,如果由于匿名内部类而未强制执行,则仅是一种风格选择,有助于记录变量的预期范围。下面的测试证实了这些信息。

1:如果编译器可以处理它,使用final会产生差异:

看一下这个片段:

boolean zZ = true;
while (zZ) {
    int xX = 1001;         // <------------- xX
    int yY = 1002;         // <------------- yY
    zZ = (xX == yY);
}

两个int变量,xXyY。第一次声明时都被声明为final,第二次则从两者中去掉了final。以下是生成的字节码(使用javap -c打印):

两者均为final

     0: iconst_1             // pushes int 1 (true) onto the stack
     1: istore_1             // stores the int on top of the stack into var zZ
     2: goto          15
     5: sipush        1001   // pushes 1001 onto the operand stack
     8: istore_2             // stores on xX
     9: sipush        1002   // pushes 1002 onto the operand stack
    12: istore_3             // stores on yY
    13: iconst_0             // pushes 0 (false): does not compare!! <---------
    14: istore_1             // stores on zZ
    15: iload_1              // loads zZ
    16: ifne          5      // goes to 5 if top int (zZ) is not 0
    19: return        

两者均非final

    // 0: to 12: all the same
    13: iload_2              // pushes xX onto the stack
    14: iload_3              // pushes yY onto the stack
    15: if_icmpne     22     // here it compares xX and yY! <------------
    18: iconst_1      
    19: goto          23
    22: iconst_0      
    23: istore_1      
    24: iload_1       
    25: ifne          5
    28: return        

在上述情况下,当它们是final时,编译器知道它们不相等并且永远不会进行比较(无论何时xX == yY都会生成false字节码)。从这个可以得出结论,就字节码而言,使用final时编译器确实可以对生成的代码进行一些优化。(我不是说它们有意义,但是final在这里绝不仅仅是一种风格选择。)

2:如果编译器无法得出结论,则在本地变量上使用final仅是一种设计选择:

现在看下面的代码:

boolean zZ = true;
int aA = 1001;
int bB = 1002;
while (zZ) {
    final int xX = aA;   // <------- took away the "final" here, didnt matter
    final int yY = bB;   // <------- took away the "final" here, didnt matter
    zZ = (xX == yY);
}

在这种情况下,即使使用了final,编译器也无法在编译时确定xXyY是否相等,对吗?
因此,我们可以看到:当我们使用或不使用final生成类时,生成的字节码完全相同(相同的MD5!)。
虽然在一般情况下,有人说使用final有性能优势,其他人则持反对意见,但在局部块中,final绝对只是一种风格选择。

3:循环内或外的局部变量-没有任何区别:

此片段生成的字节码...

boolean zZ = true;
int aA = 1001, bB = 1002;
while (zZ) {
    int xX = aA;                      // <--- declaration is inside WHILE
    int yY = bB;
    zZ = (xX == yY);
}

...以及此片段生成的字节码...

boolean zZ = true;
int aA = 1001, bB = 1002;
int xX, yY;                           // <--- declaration is outside WHILE
while (zZ) {
    xX = aA;
    yY = bB;
    zZ = (xX == yY);
}

...是完全相同的(当然,只有行号改变了)。

使用对象进行的其他测试(不仅限于原始类型变量)表现出相同的行为。

因此可以安全地得出结论,如果没有在其他地方使用,在循环内部或外部声明局部变量基本上是一种设计选择,没有字节码效果。

注意:所有测试都是在Oracle的JRE版本1.7.0_13下进行的。


  1. 仅通过查看字节码文件来得出Java优化代码的能力是不明智的。严肃的优化工作是由JIT编译器在运行时完成的。
  2. 实际上,JIT编译器完全有能力判断变量是否超出某个点后不再改变。添加“final”可能并不能帮助它。
- Stephen C
@StephenC 感谢您的建议,我很感激。我必须承认,从字节码文件中得出结论似乎不明智。但是你看,我只得出了生成的字节码相同的结论(有或没有 final)。一旦 JIT 接管它,它就无法知道是否存在 final 关键字。因此,这不会有任何影响。您觉得呢? - acdcjunior
1
你是正确的。理论上,如果有差异,它们可以被编码在其他描述符中,但从我对JVM规范的简要阅读来看,它们并没有... - Stephen C
@StephenC,你们是在暗示即使对于具有非最终本地变量的类生成的字节码不同,即它确实指示JIT在运行时执行两个值的比较步骤,但JIT可能会进一步优化它(看到变量不再被重新分配)并跳过运行时的比较步骤吗? - sactiw
是的,这就是我想说的。我还没有检查本地代码以查看它是否实际上执行了该操作,但如果可以的话,我肯定会期望JIT编译器优化掉测试。 - Stephen C

4

final是常量变量的关键字。声明为final可以防止您在循环中重新分配它。

temp将在每次迭代中重新声明,无论它是否为final。

例如:

while (...)
{
    final int temp = ...;

    temp = 5; // compiler error
}

但如果它不是常量(终态):

while (...)
{
    int temp = ...;

    temp = 5; // fine
}

所以这只是对自己的一个提示:“我没有修改这个变量的意图”?并且不会影响运行时间? - PM 77-1
@PM77-1 这里有一些关于那个问题的好答案:https://dev59.com/J2855IYBdhLWcg3wilCD - Supericy
谢谢。这个我还没有见过。 - PM 77-1
它是错误的,在我的情况下它正在改变它的值,请参考此链接-https://stackoverflow.com/questions/57175610/after-service-call-automatically-variable-value-changing-in-loop - sachin

1
考虑一个完全不同的视角:在函数式编程语言中,几乎所有的赋值都是最终的,并且类是不可变的。这意味着非最终的赋值和/或可变的类是例外
如果您的代码是用Scala编写的,IntelliJ IDE会显示提示“可以将此赋值更改为final”。
我真的很欣赏“finals”,因为如果您以后阅读代码,您会在第一眼看到这个赋值从未在下面的某些行中更改。如果您知道实例是不可变的,这也会有所帮助。
此外,如果您始终使用“finals”,那么非最终的变量将会变得更加明显,而这些变量通常是最重要的观察对象。

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