最终和有效最终之间的区别


350

我在用Java 8玩lambda,遇到警告local variables referenced from a lambda expression must be final or effectively final。我知道,当我在匿名类中使用变量时,它们必须在外部类中为final,但仍然-final有效的final之间有什么区别?


2
答案很多,但实际上基本上都是“没有区别”。但这是真的吗?不幸的是,我似乎无法找到一个语言规范的Java 8
亚历山大·杜宾斯基

3
@AleksandrDubinsky docs.oracle.com/javase/specs
EIS

@AleksandrDubinsky不是“真的”是真的。我发现此规则有一个例外。用常量初始化的局部变量不是编译器的常量表达式。在显式添加final关键字之前,不能在开关/案例中为案例使用此类变量。例如“ int k = 1; switch(someInt){case k:...”。
Henno Vermeulen 2015年

Answers:


234

...从Java SE 8开始,本地类可以访问最终变量或有效最终变量的局部变量和封闭块的参数。变量或参数的值在初始化后从未更改,实际上是最终的。

例如,假设numberLength未将变量声明为final,则在PhoneNumber构造函数中添加了标记的赋值语句:

public class OutterClass {  

  int numberLength; // <== not *final*

  class PhoneNumber {

    PhoneNumber(String phoneNumber) {
        numberLength = 7;   // <== assignment to numberLength
        String currentNumber = phoneNumber.replaceAll(
            regularExpression, "");
        if (currentNumber.length() == numberLength)
            formattedPhoneNumber = currentNumber;
        else
            formattedPhoneNumber = null;
     }

  ...

  }

...

}

由于该赋值语句,变量numberLength不再有效地变为final。结果,Java编译器生成一条错误消息,类似于“内部类引用的本地变量必须是最终的或实际上是最终的”,其中内部类PhoneNumber尝试访问numberLength变量:

http://codeinventions.blogspot.in/2014/07/difference-between-final-and.html

http://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html


68
+1注意:如果引用未更改,则即使引用的对象已更改,它实际上也是最终的。
彼得·劳瑞

1
@stanleyerror这可能会有所帮助:stackoverflow.com/questions/4732544/...

1
我觉得比的例子更为有用没有有效的最后,是当事情的一个例子有效的决赛。尽管描述确实使之很清楚。如果没有代码更改其值,则不必将Var声明为final。
Skychan

1
该示例不正确。这段代码可以完美地编译(当然没有点)。为了获得编译器错误,此代码应位于某个方法内,以使其numberLength成为此方法的局部变量。
mykola

1
为什么这个例子如此复杂有原因吗?为什么大多数代码都处理完全不相关的正则表达式操作?而且,正如@mykola已经说过的那样,它完全缺少有效的最终属性的标记,因为这仅与局部变量有关,在此示例中没有局部变量。
Holger

130

我发现解释“有效最终”的最简单方法是想象将final修饰符添加到变量声明中。如果通过此更改,程序在编译时和运行时都继续以相同的方式运行,则该变量实际上是最终变量。


4
只要对Java 8的“最终版本”的理解得到了很好的理解,这是正确的。否则,我将查看一个未声明为final的变量,您稍后对其进行了赋值,并错误地认为它不是final。您可能会说“当然”……但是并不是每个人都应该尽可能多地关注最新的语言版本更改。
fool4jesus 2015年

8
此规则的一个例外是,用常量初始化的局部变量不是编译器的常量表达式。在显式添加final关键字之前,不能在开关/案例中为案例使用此类变量。例如“ int k = 1; switch(someInt){case k:...”。
Henno Vermeulen 2015年

2
@HennoVermeulen switch-case并非此答案中规则的例外。该语言指定case k需要一个常量表达式,该常量表达式可以是常量变量(“常量变量是原始类型或使用常量表达式初始化的String类型的最终变量” JLS 4.12.4),这是final的特殊情况。变量。
Colin D Bennett '18

3
在我的示例中,编译器抱怨k不是常数表达式,因此不能将其用于开关。添加final时,编译行为会更改,因为它现在是一个常量变量,可以在开关中使用。因此,您是对的:规则仍然正确。它根本不适用于此示例,也没有说k是否有效地为最终值。
Henno Vermeulen

36

根据文档

变量或参数的值在初始化后从未更改,实际上是最终的。

基本上,如果编译器发现变量没有在其初始化之外的赋值中出现,则该变量实际上被认为是final

例如,考虑一些类:

public class Foo {

    public void baz(int bar) {
        // While the next line is commented, bar is effectively final
        // and while it is uncommented, the assignment means it is not
        // effectively final.

        // bar = 2;
    }
}

该文档讨论局部变量。bar在您的示例中,不是本地变量,而是字段。上面的错误消息中的“有效最终”根本不适用于该字段。
Antti Haapala 2015年

6
@AnttiHaapala bar是此处的参数,而不是字段。
peter.petrov

30

“有效地最终”是一个变量,如果要附加“最终”,则不会给出编译器错误

从“ Brian Goetz”的文章中,

非正式地,如果局部变量的初始值从未改变,则它实际上是最终的-换句话说,声明它的最终不会导致编译失败。

拉姆达州决赛-布莱恩·格茨


2
此答案显示为引号,但是Brian的文章中没有这样的确切文字,请确保没有附加单词。而是用引号引起来:非正式地,如果局部变量的初始值从未改变,则它实际上是最终的-换句话说,声明它的最终不会导致编译失败。
lcfd

从逐字记录中摘录:非正式地,如果局部变量的初始值从未更改,则它实际上是最终变量-换句话说,声明为最终变量不会导致编译失败。
Ajeet Ganga '18

26

下面的这个变量是final,所以初始化后就无法更改其值。如果我们尝试这样做,将会出现编译错误...

final int variable = 123;

但是,如果我们创建这样的变量,我们可以更改它的值...

int variable = 123;
variable = 456;

但是在Java 8中,默认情况下所有变量都是final。但是代码中第二行的存在使它成为非最终的。因此,如果我们从上面的代码中删除第二行,那么我们的变量现在是“有效的最终值” ...

int variable = 123;

因此,任何仅分配一次的变量都是“有效最终”变量


答案应该如此简单。
superigno '19

@Eurig,“所有变量默认为最终变量”都需要引用。
Pacerier

10

变量是最终的有效的最后,当它的初始化一次,它从来没有突变在其主阶级。而且我们不能循环内部类中对其进行初始化

决赛

final int number;
number = 23;

有效地最终

int number;
number = 34;

注意Final有效Final相似(赋值后它们的值不变),只是有效Final变量没有用Keyword声明final


7

当lambda表达式从其封闭空间使用分配的局部变量时,存在一个重要的限制。Lambda表达式只能使用其值不变的局部变量。该限制称为“ 变量捕获 ”,描述为:lambda表达式捕获值,而不是变量
lambda表达式可以使用的局部变量称为“ 有效最终 ”。
有效的最终变量是在首次赋值后其值不变的变量。无需将此类变量显式声明为final,尽管这样做不会出错。
让我们看一个例子,我们有一个局部变量i,它的初始值是7,在lambda表达式中,我们试图通过给i赋一个新值来改变该值。这将导致编译器错误-“ 在封闭范围内定义的局部变量必须是最终的或实际上是最终的

@FunctionalInterface
interface IFuncInt {
    int func(int num1, int num2);
    public String toString();
}

public class LambdaVarDemo {

    public static void main(String[] args){             
        int i = 7;
        IFuncInt funcInt = (num1, num2) -> {
            i = num1 + num2;
            return i;
        };
    }   
}

2

有效的最终主题在JLS 4.12.4中进行了描述,最后一段包含清晰的解释:

如果变量实际上是final变量,则将final修饰符添加到其声明中不会引入任何编译时错误。相反,如果删除了final修饰符,则在有效程序中声明为final的局部变量或参数实际上将变为final。


2

final是使用关键字声明的变量final,例如:

final double pi = 3.14 ;

它仍然final贯穿整个程序。

最终有效:任何局部变量或参数,现在仅分配一次值(或仅更新一次)。在整个程序中,它可能无法保持有效的最终状态。因此,这意味着有效最终变量可能会在分配/更新至少一个以上分配后立即失去其有效最终属性。例:

class EffectivelyFinal {

    public static void main(String[] args) {
        calculate(124,53);
    }

    public static void calculate( int operand1, int operand2){   
     int rem = 0;  //   operand1, operand2 and rem are effectively final here
     rem = operand1%2  // rem lost its effectively final property here because it gets its second assignment 
                       // operand1, operand2 are still effectively final here 
        class operators{

            void setNum(){
                operand1 =   operand2%2;  // operand1 lost its effectively final property here because it gets its second assignment
            }

            int add(){
                return rem + operand2;  // does not compile because rem is not effectively final
            }
            int multiply(){
                return rem * operand1;  // does not compile because both rem and operand1 are not effectively final
            }
        }   
   }    
}

根据Java语言规范,这是不正确的:“ 只要它出现在赋值表达式的左侧,就肯定是未赋值的,而且在赋值之前也不是绝对赋值的。” 变量/参数要么总是最终值,要么永远不是有效值。更明确地讲,如果您不能在final不引入编译错误的情况下将关键字添加到声明中,那么它实际上不是final。这与该语句相反:“如果变量实际上是最终变量,则在其声明中添加final修饰符不会引入任何编译时错误。”
AndrewF

由于我的注释中所述的所有原因,示例代码中的注释不正确。“有效最终”不是可以随时间变化的状态。
AndrewF

@AndrewF如果它不会随时间变化,那么您认为最后一行不会编译吗?在计算方法中,rem实际上是第1行的最终结果。但是,在最后一行,编译器抱怨rem并不是有效的最终
方法

您是正确的,需要从代码块中删除一些代码才能进行编译,但这并不反映运行时行为。在编译时,您可以确定变量是否有效地为最终变量-根据规范,该变量要么始终是有效最终值,要么永远不是有效最终值。编译器可以通过静态查看变量在整个作用域中的使用情况来说明。程序运行时不能获得或丢失该属性。该术语在规范中定义明确-请查看其他答案,这些解释都很好。
AndrewF

1
public class LambdaScopeTest {
    public int x = 0;        
    class FirstLevel {
        public int x = 1;    
        void methodInFirstLevel(int x) {

            // The following statement causes the compiler to generate
            // the error "local variables referenced from a lambda expression
            // must be final or effectively final" in statement A:
            //
            // x = 99; 

        }
    }    
}

就像其他人所说的那样,变量或参数的值在初始化之后再也不会改变,实际上是最终的。在上面的代码中,如果您更改x内部类中的值,FirstLevel则编译器将给您错误消息:

从lambda表达式引用的局部变量必须是final或有效的final。


1

如果可以将final修饰符添加到局部变量,则它实际上是 最终的。

Lambda表达式可以访问

  • 静态变量

  • 实例变量

  • 有效的最终方法参数,以及

  • 有效地最终局部变量。

来源:OCP:Oracle认证专业Java SE 8程序员II学习指南,Jeanne Boyarsky,Scott Selikoff

另外,

一个effectively final变量是一个变量,其值永远不会改变,但它不与声明的final关键字。

资料来源:从Java入手:从控制结构到对象(第6版),Tony Gaddis

此外,不要忘记final在第一次使用它之前将其初始化一次的含义。


0

声明变量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(Windows 64位上的Java 8u161):

  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并且不会改变它的状态。因此,此代码不可访问(在此答案中更多)。出于同样的原因,行上的代码9也丢失了。i因为是true肯定的,所以不必评估的状态。

另一方面,尽管变量j实际上是最终变量,但它的处理方式不同。没有应用此类优化。的状态j被评估两次。字节码是不分相同的j有效的最后


我认为这是一种编译器效率低下的情况,不一定是在较新的编译器中仍然如此的情况。在一个完美的编译中,如果一个变量实际上是最终变量,那么它将生成与一个声明的最终变量完全相同的优化。因此,不要依赖于有效的final自动比声明final慢的想法。
AndrewF

@AndrewF通常,您是对的,行为可能会更改。这就是为什么我写了“ 可能会导致(取决于编译器)使用不同的字节码 ”的原因。仅由于缺少优化(不同的字节码),我才不会认为执行速度会变慢。但是在所示情况下仍然有所不同。
LuCio

-6

但是,从Java SE 8开始,本地类可以访问最终或有效最终的>封闭块的局部变量和参数。

这不是从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 2015年

我只看到代码不能正常工作,无论您使用的是Java 7和Java 8
霍尔格·
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.