Final vs Effectively Final in Java – 在Java中的最终与有效的最终

最后修改: 2020年 1月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

One of the most interesting features introduced in Java 8 is effectively final. It allows us to not write the final modifier for variables, fields, and parameters that are effectively treated and used like final ones.

Java 8中引入的最有趣的功能之一是有效的final。它允许我们不为变量、字段和参数写final修饰符,这些变量、字段和参数被有效地处理和使用。

In this tutorial, we’ll explore this feature’s origin and how it’s treated by the compiler compared to the final keyword. Furthermore, we’ll explore a solution to use regarding a problematic use-case of effectively final variables.

在本教程中,我们将探讨这一特性的起源,以及与finalkeyword相比,编译器是如何对待它的。此外,我们将探讨一个关于有效的最终变量的问题用例的解决方案。

2. Effectively Final Origin

2.有效的最终起源

In simple terms, objects or primitive values are effectively final if we do not change their values after initialization. In the case of objects, if we do not change the reference of an object, then it is effectively final — even if a change occurs in the state of the referenced object.

简单地说,如果我们在初始化之后不改变对象或原始值的值,那么它们实际上是最终的。在对象的情况下,如果我们不改变对象的引用,那么它实际上是最终的–即使引用对象的状态发生了变化。

Prior to its introduction, we could not use a non-final local variable in an anonymous class. We still cannot use variables that have more than one value assigned to them inside anonymous classes, inner classes, and lambda expressions. The introduction of this feature allows us to not have to use the final modifier on variables that are effectively final, saving us a few keystrokes.

在它被引入之前,我们不能在匿名类中使用非最终局部变量。我们仍然不能在匿名类、内部类和lambda表达式中使用那些被分配了一个以上的值的变量。这一特性的引入使我们不必在实际上是最终的变量上使用final修饰符,为我们节省了一些按键。

Anonymous classes are inner classes and they cannot access non-final or non-effectively-final variables or mutate them in their enclosing scopes as specified by JLS 8.1.3. The same limitation applies to lambda expressions, as having access can potentially produce concurrency issues.

匿名类是内层类,它们不能访问非最终变量或非有效最终变量,也不能按照JLS 8.1.3.的规定在其包围范围内突变这些变量。同样的限制也适用于lambda表达式,因为有访问可能产生并发问题。

3. Final vs Effectively Final

3.最终与有效的最终

The simplest way to understand whether a final variable is effectively final is to think whether removing the final keyword would allow the code to compile and run:

理解一个终结变量是否有效的终结,最简单的方法是思考去掉final关键字是否能让代码编译和运行。

@FunctionalInterface
public interface FunctionalInterface {
    void testEffectivelyFinal();
    default void test() {
        int effectivelyFinalInt = 10;
        FunctionalInterface functionalInterface 
            = () -> System.out.println("Value of effectively variable is : " + effectivelyFinalInt);
    }
}

Reassigning a value or mutating the above effectively final variable would make the code invalid regardless of where it occurs.

重新赋值或突变上述有效的最终变量将使代码无效,无论它发生在哪里。

3.1. Compiler Treatment

3.1.编译器处理

JLS 4.12.4 states that if we remove the final modifier from a method parameter or a local variable without introducing compile-time errors, then it’s effectively final. Moreover, if we add the final keyword to a variable’s declaration in a valid program, then it’s effectively final.

JLS 4.12.4指出,如果我们从一个方法参数或局部变量中移除final修饰符而不引入编译时错误,那么它实际上就是最终的。此外,如果我们将final关键字添加到一个有效程序中的变量声明中,那么它实际上是最终的。

The Java compiler doesn’t do additional optimization for effectively final variables, unlike it does for final variables.

Java编译器并不像对最终变量那样,对有效的最终变量进行额外的优化。

Let’s consider a simple example that declares two final String variables but only uses them for concatenation:

让我们考虑一个简单的例子,它声明了两个final String变量,但只将它们用于连接。

public static void main(String[] args) {
    final String hello = "hello";
    final String world = "world";
    String test = hello + " " + world;
    System.out.println(test);
}

The compiler would change the code executed in the main method above to:

编译器将把上述main方法中执行的代码改为:

public static void main(String[] var0) {
    String var1 = "hello world";
    System.out.println(var1);
}

On the other hand, if we remove the final modifiers, the variables would be considered effectively final, but the compiler won’t remove them since they’re only used for concatenation.

另一方面,如果我们删除final修饰符,这些变量将被认为是有效的final,但是编译器不会删除它们,因为它们只用于连接。

4. Atomic Modification

4.原子修饰

Generally, it’s not a good practice to modify variables used in lambda expressions and anonymous classes. We cannot know how these variables are going to be used inside method blocks. Mutating them might lead to unexpected results in multithreading environments.

一般来说,修改lambda表达式和匿名类中使用的变量不是一个好的做法。我们无法知道这些变量将在方法块中被如何使用。在多线程环境下,突变它们可能会导致意想不到的结果。

We already have a tutorial explaining the best practices when using lambda expressions and another that explains common anti-patterns when we modify them. But there’s an alternative approach that allows us to modify variables in such cases that achieves thread-safety through atomicity.

我们已经有一篇教程解释了使用lambda表达式时的最佳做法,还有一篇教程解释了我们修改这些变量时常见的反模式。但是有一种替代方法,允许我们在这种情况下修改变量,通过原子性实现线程安全。

The package java.util.concurrent.atomic offers classes such as AtomicReference and AtomicInteger. We can use them to atomically modify variables inside lambda expressions:

java.util.concurrent.atomic提供了诸如AtomicReferenceAtomicInteger等类。我们可以用它们来原子化地修改λ表达式中的变量。

public static void main(String[] args) {
    AtomicInteger effectivelyFinalInt = new AtomicInteger(10);
    FunctionalInterface functionalInterface = effectivelyFinalInt::incrementAndGet;
}

5. Conclusion

5.总结

In this tutorial, we learned about the most notable differences between final and effectively final variables. In addition, we provided a safe alternative that allows us to modify variables inside lambda functions.

在本教程中,我们了解了final和有效final变量之间最显著的区别。此外,我们提供了一个安全的替代方案,允许我们在lambda函数内修改变量。