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个回答

253
从Java SE 8开始,局部类可以访问封闭块的本地变量和参数,这些变量和参数是final或有效final的。一个在初始化后值从未改变的变量或参数就是有效final的。
例如,假设变量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。因此,在内部类PhoneNumber试图访问numberLength变量时,Java编译器会生成类似于“局部变量从内部类引用必须是final或者有效final”的错误信息。

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

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


82
+1 注意:如果一个引用未被改变,那么即使所引用的对象已经改变,该引用仍然是有效的。 - Peter Lawrey
1
@stanleyerror 这可能会有所帮助:https://dev59.com/5G445IYBdhLWcg3ws8Zp - user801154
1
我认为比一个有效地被定义的例子更有用的是一个有效地被定义的例子。尽管描述已经很清楚了。如果没有代码改变它的值,那么变量不需要被声明为final。 - Skystrider
3
例子是错误的。这段代码可以完美编译(当然,没有点号)。要获取编译器错误,此代码应该位于某个方法内,以便numberLength成为该方法的局部变量。 - mykola
2
这个例子为什么这么复杂?为什么大部分代码都在处理完全无关的正则表达式操作?正如@mykola已经说过的那样,它完全没有涉及到“effective final”属性,因为它只与局部变量有关,而在这个例子中没有局部变量。 - Holger
显示剩余5条评论

145

我认为解释“ effectively final ”最简单的方法是想象在变量声明中添加 final 修饰符。 如果进行此更改,程序在编译时和运行时继续以相同的方式工作,则该变量就是 effectively final。


4
只要理解了Java 8中的“final”,这就是真的。否则,如果你看到一个没有声明为final的变量,稍后又做了赋值操作,你可能会错误地认为它不是final。你可能会说“当然”......但并不是每个人都像应该关注最新的语言版本变化一样。 - fool4jesus
9
这个规则有一个例外,即本地变量如果使用常量进行初始化,则编译器不会将其视为常量表达式。在switch/case语句中,你不能使用这样的变量作为case条件,除非你显式地添加final关键字。例如:"int k = 1; switch(someInt) { case k: ...". - Henno Vermeulen
2
@HennoVermeulen switch-case在这个答案中也不例外。该语言指定case k需要一个_常量表达式_,它可以是_常量变量_(“常量变量是使用常量表达式初始化的原始类型或String类型的final变量”[JLS 4.12.4](https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4)),这是final变量的一种特殊情况。 - Colin D Bennett
3
在我的例子中,编译器抱怨k不是一个常量表达式,因此不能用于switch语句。当添加final时,编译行为会改变,因为它现在是一个常量变量,可以在switch语句中使用。所以你是对的:规则仍然正确。它只是不适用于这个例子,并且并不说明k是否是有效地final。 - Henno Vermeulen

49

下面这个变量是final的,一旦初始化后我们就无法改变它的值。如果我们试图修改,将会出现编译错误...

final int variable = 123;

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

int variable = 123;
variable = 456;

然而在Java 8中,默认情况下所有的变量都是final的,但代码中第二行的存在使其成为非 final的。因此,如果我们从上述代码中删除第二行,则该变量现在是"有效 final"的...

int variable = 123;

所以.. 任何只被赋值一次的变量,都被视为“有效地是final的”.


就像答案应该是一样简单。 - superigno
2
@Eurig,“所有变量默认都是final”的引用需要提供。 - Pacerier
1
为什么它们默认是final的,当我们可以轻松地更改它们的值并有效地“覆盖”final概念呢? - Stefan
在Java 8中,“所有变量默认都是final”的说法不仅具有误导性,而且是完全错误的。变量默认情况下可以随意重新赋值,因此并非是final的。 - christopheml

39
根据文档

一个在初始化后值从未改变的变量或参数被视为有效的 final。

基本上,如果编译器发现一个变量在其初始化之外没有出现赋值操作,则该变量被视为有效的 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;
    }
}

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Antti Haapala -- Слава Україні
6
@AnttiHaapala 这里的 bar 是一个参数,而不是一个字段。 - peter.petrov

39

'Effectively final'是指一个变量在没有被标注为final的情况下,也不会给编译器造成错误。

来自'Brian Goetz'的文章,

非正式地说,如果一个局部变量的初始值从未改变,则其“有效地”为final——换句话说,声明它为final不会导致编译失败。

lambda-state-final- Brian Goetz


3
这个答案被显示为引用,然而在Brian的文章中并没有确切的这段文字,肯定不是“appended”这个词。这是一个引用:“非正式地说,如果局部变量的初始值从未更改,则其实际上是最终的 - 换句话说,声明它为final不会导致编译失败。” - lcfd
非正式地说,如果局部变量的初始值从未更改,则其实际上是final的 - 换句话说,声明为final不会导致编译失败。 - Ajeet Ganga

12

当一个变量在其所属的类中被初始化一次且从未被修改时,它被称为final有效地final。而且我们不能在循环或内部类中初始化它。

Final:

final int number;
number = 23;

有效的终态:

int number;
number = 34;

注意:最终值和有效最终值非常相似(它们在赋值后的值不会改变),只是有效最终变量没有使用关键字final进行声明。


10
当lambda表达式使用来自其封闭空间的分配本地变量时,有一个重要的限制。Lambda表达式只能使用值不会改变的本地变量。这个限制被称为“变量捕获”,它被描述为; lambda表达式捕获值,而不是变量。
Lambda表达式可以使用的本地变量称为“有效最终变量”。有效最终变量是指其值在首次赋值后不会更改的变量。不需要显式声明此类变量为final,但这样做也不会出错。
让我们通过一个例子来看看,我们有一个本地变量i,它的初始值为7,在lambda表达式中,我们试图通过将一个新值分配给i来更改该值。这将导致编译器错误-“在封闭范围内定义的局部变量i必须是final或有效的最终变量”。
@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;
        };
    }   
}

3

Effective final主题在JLS 4.12.4中有描述,最后一段提供了清晰的解释:

如果变量是有效的final,那么在其声明中添加final修饰符不会引入任何编译时错误。相反,如果一个局部变量或参数在有效程序中被声明为final,那么如果去除final修饰符,它将成为有效的final。


2

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

final double pi = 3.14 ;

在整个程序中它是不可变的,不能在此行之后更改pi。

有效地 final: 任何只被赋值一次或者只更新一次的局部变量或参数。它可能不会在整个程序中保持有效地 final。因此,这意味着有效地 final变量在至少进行一次分配/更新之后立即失去了其有效地 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修饰符添加到其声明中不会引入任何编译时错误。” - AndrewF
示例代码中的注释有误,原因在我的评论中已经描述。 "Effectively final" 不是一个随时间可以改变的状态。 - AndrewF
@AndrewF 如果它不随时间改变,你认为最后一行为什么无法编译?在calculate方法中,rem在第1行被有效地声明为final。然而,在最后一行,编译器抱怨rem不再是有效的final。 - The Scientific Method
你说得对,为了编译通过,必须从代码块中删除一些代码,但这并不反映运行时行为。在编译时,你可以根据规范决定变量是否是有效 final 的——基于规范,它要么 始终 是有效 final 的,要么 从不 是有效 final 的。编译器可以通过静态地查看变量在其作用域内的使用情况来判断。该属性在程序运行时无法获取或丢失。该术语已被规范明确定义——查看其他答案,它们解释得很清楚。 - 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; 

        }
    }    
}

正如其他人所说,一个变量或参数在初始化后其值不再改变就被称为有效final。在上述代码中,如果您更改内部类FirstLevel中的x的值,则编译器将会给出错误消息:
“来自lambda表达式的本地变量必须是final或有效final。”

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