final和effectively final的区别

388

我正在使用Java 8中的lambda表达式,并遇到警告本地变量从lambda表达式引用必须是final或有效 final。我知道当我在匿名类中使用变量时,它们必须在外部类中是final的,但仍然存在问题-什么是final有效final之间的区别?


2
很多答案都基本上是“没有区别”,但这真的是真的吗?不幸的是,我似乎找不到Java 8的语言规范。 - Aleksandr Dubinsky
3
@AleksandrDubinsky https://docs.oracle.com/javase/specs/ - eis
1
@AleksandrDubinsky 不是“真的”正确。我发现了一个例外情况。使用常量初始化的局部变量对编译器来说不是常量表达式。在switch/case中,您不能使用这样的变量作为case,除非您明确添加final关键字。例如,“int k = 1; switch(someInt) { case k: ...”。 - Henno Vermeulen
15个回答

1
如果您可以将final修饰符添加到局部变量中,则该变量就是“有效地final”。Lambda表达式可以访问静态变量、实例变量、有效的final方法参数和有效的final局部变量。
此外,“有效的final”变量是一个值从未更改但未声明为final关键字的变量。
来源:OCP:Oracle认证专业Java SE 8程序员II学习指南,Jeanne Boyarsky,Scott Selikoff 来源:从控制结构到对象(第6版),Tony Gaddis开始使用Java 此外,不要忘记 final 的含义,即它在第一次使用之前只被初始化一次。

0

声明一个变量为final或不声明为final,但保持其有效的final状态,可能会导致(取决于编译器)不同的字节码。

让我们看一个小例子:

    public static void main(String[] args) {
        final boolean i = true;   // 6  // final by declaration
        boolean j = true;         // 7  // effectively final

        if (i) {                  // 9
            System.out.println(i);// 10
        }
        if (!i) {                 // 12
            System.out.println(i);// 13
        }
        if (j) {                  // 15
            System.out.println(j);// 16
        }
        if (!j) {                 // 18
            System.out.println(j);// 19
        }
    }

主方法main的相应字节码(Java 8u161,Windows 64位):

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_1
       3: istore_2
       4: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: iconst_1
       8: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      11: iload_2
      12: ifeq          22
      15: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
      18: iload_2
      19: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      22: iload_2
      23: ifne          33
      26: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
      29: iload_2
      30: invokevirtual #22                 // Method java/io/PrintStream.println:(Z)V
      33: return

相应的行号表:

 LineNumberTable:
   line 6: 0
   line 7: 2
   line 10: 4
   line 15: 11
   line 16: 15
   line 18: 22
   line 19: 26
   line 21: 33

当我们查看第121314行的源代码时,字节码中并没有出现。这是因为itrue,并且不会改变其状态。因此,这段代码是无法访问的(更多信息请参见answer)。由于同样的原因,第9行的代码也缺失了。不必评估i的状态,因为它肯定是true

另一方面,尽管变量j有效地最终的,但处理方式不同。没有应用这样的优化。j的状态被评估了两次。无论j是否为有效地最终,字节码都是相同的。


我认为这是编译器效率低下的表现,但不一定在新版编译器中仍然如此。在完美的编译过程中,如果变量是有效 final 的,则会生成与声明 final 相同的所有优化。因此,不要依赖有效 final 自动比声明 final 更慢的概念。 - AndrewF
@AndrewF 一般来说,你是正确的,行为可能会改变。这就是为什么我写了“_可能会导致(取决于编译器)不同的字节码_”。仅仅因为缺少优化(不同的字节码),我不会认为执行会更慢。但在所示的情况下仍然存在差异。 - LuCio

0

有效终态变量是一个本地变量,它:

  1. 没有被定义为 final
  2. 只被赋值一次。

而 final 变量是一个被声明为:

  1. 带有 final 关键字的变量。

0
在Java中,有三种类型的最终变量声明。
1. 隐式声明的最终变量
有三种隐式声明的最终变量。
- 在接口中声明的字段。 - 作为try-with-resource语句中的资源声明的局部变量。 - 多重try子句中的异常参数。
2. 显式声明的最终变量
这些是使用"final"关键字声明的变量。
3. 有效最终变量
如果满足以下条件,变量被视为有效最终变量:
- 在语句中声明的局部变量,并且其声明符具有初始化程序,或者通过模式声明的局部变量,并且应遵循以下规则: 1. 没有使用"final"进行声明。 2. 从未出现过赋值操作。 3. 从未出现在前缀或后缀递增或递减运算符的操作数中。
例如:在语句中声明的局部变量,并且其声明符具有初始化程序。
List<String> ls = Arrays.asList("A", "B", "C");

for (Iterator<String> iterator = ls.iterator(); iterator.hasNext();) {
  String s = (String) iterator.next();
  System.out.println(s);
}

这里变量iterator是有效地终态,因为它遵循上述规则。

注意如果变量是有效地终态,也可以使用final关键字声明。

List<String> ls = Arrays.asList("A", "B", "C");

for (final Iterator<String> iterator = ls.iterator(); iterator.hasNext();) {
  String s = (String) iterator.next();
  System.out.println(s);
}

ex: 模式声明的局部变量

public String getStringfromObj(Object o){

    if (o instanceof String s && s.startsWith("Java")) { // can be declared as final String s
      return s;
    }
    
    return "";
}

ii. 在语句中声明的局部变量,稍后初始化。应遵循以下规则。
  1. 不使用 final 声明
  2. 在明确赋值之前和之后不作为值分配出现
  3. 不作为前缀或后缀递增或递减运算符的操作数出现

示例:

List<String> ls = Arrays.asList("A", "B", "C");
int k; // can be declared as final int k
    
    if ((k = ls.size()) > 0) {
      System.out.println(k);
}

iii. 具有初始化程序的声明符的方法、构造函数、lambda表达式或异常参数。

-6
然而,从Java SE 8开始,本地类可以访问封闭块的局部变量和参数,这些变量和参数是final或有效final的。这并不是从Java 8开始的,我很久以前就使用了这个功能。 在Java 8之前,以下代码是合法的:
String str = ""; //<-- not accesible from anonymous classes implementation
final String strFin = ""; //<-- accesible 
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
         String ann = str; // <---- error, must be final (IDE's gives the hint);
         String ann = strFin; // <---- legal;
         String str = "legal statement on java 7,"
                +"Java 8 doesn't allow this, it thinks that I'm trying to use the str declared before the anonymous impl."; 
         //we are forced to use another name than str
    }
);

2
该语句指的是在Java 8之前,只能访问“final”变量,但在Java 8中也可以访问那些“有效地”是最终的变量。 - Antti Haapala -- Слава Україні
我只看到代码不工作,无论您使用的是Java 7还是Java 8。 - Holger

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