本地变量需要 final,实例变量不需要

在 lambda 中,局部变量需要是 final,但实例变量不需要,为什么呢?

86582 次浏览

看起来你是在询问可以从 lambda 体引用的变量。

来自 JLS 15.27.2

在 lambda 表达式中使用但未声明的任何局部变量、形式参数或异常参数必须声明为 final 或实际上为 final (4.12.4) ,否则在尝试使用时会发生编译时错误。

所以你不需要声明变量为 final,你只需要确保它们是“有效的最终”。这与适用于匿名类的规则相同。

字段和局部变量之间的根本区别在于,当 JVM 创建 lambda 实例时,局部变量是 复制。另一方面,字段可以自由更改,因为对它们的更改也会传播到外部类实例(正如 Boris 在下面指出的,它们的 范围是整个外部类)。

考虑匿名类、闭包和 labmdas 最简单的方法是从 可变作用域的角度,想象一下为传递给闭包的所有局部变量添加一个复制建构子。

在项目文件 lambda: Lambda v4的状态

7. 变量捕获章节中提到... 。

我们的目的是禁止捕获可变的局部变量 原因是这样的成语:

int sum = 0;
list.forEach(e -> { sum += e.size(); });

基本上是串行的; 写 lambda 体是相当困难的 像这样没有种族条件。除非我们愿意 强制(最好是在编译时)这样的函数不能转义 它的捕捉线程,这个功能可能会造成更多的麻烦比它 解决了。

编辑:

这里需要注意的另一件事是,当您在内部类中访问局部变量时,它们会在内部类的构造函数中传递,这对非 final 变量不起作用,因为非 final 变量的值在构造之后可以更改。

当遇到实例变量时,编译器会传递类的引用,而类的引用会被用来存取实例变量。所以在实例变量的情况下不需要它。

PS: 值得一提的是,匿名类只能访问最终的本地变量(在 JAVA SE 7中) ,而在 JAVA SE 8中,您可以有效地访问 lambda 以及内部类中的最终变量。

因为实例变量总是通过对某个对象(即 some_expression.instance_variable)的引用的字段访问操作来访问。即使你没有像 instance_variable那样通过点符号显式地访问它,它也被隐式地看作是 this.instance_variable(或者如果你在一个内部类中访问一个外部类的实例变量,OuterClass.this.instance_variable,它位于引擎罩 this.<hidden reference to outer this>.instance_variable之下)。

因此,一个实例变量永远不会被直接访问,而你直接访问的真正的“变量”是 this(它是“有效的最终”,因为它是不可赋值的) ,或者是其他表达式开头的一个变量。

下面是一个代码示例,因为我也没有预料到这一点,所以我希望不能修改 lambda 之外的任何内容

 public class LambdaNonFinalExample {
static boolean odd = false;


public static void main(String[] args) throws Exception {
//boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error
runLambda(() -> odd = true);
System.out.println("Odd=" + odd);
}


public static void runLambda(Callable c) throws Exception {
c.call();
}


}

产出: 奇数 = 真

在 Lambda 表达式中,可以有效地使用来自周围作用域的 final 变量。 有效地意味着,不必强制声明变量 final,但要确保不会在 lambda 表达式中更改其状态。

您也可以在闭包中使用 this,使用“ this”表示封闭对象,而不是 lambda 本身,因为闭包是匿名函数,它们没有与之关联的类。

所以当你使用封闭类中的任何字段(比如私有 Integer i;) ,这个字段没有被声明为 final,也没有被有效地声明为 final,它仍然可以工作,因为编译器代表你使用了这个技巧,并且插入了“ this”(this. i)。

private Integer i = 0;
public  void process(){
Consumer<Integer> c = (i)-> System.out.println(++this.i);
c.accept(i);
}

Java8在运行中的书中,这种情况被解释为:

您可能会问自己为什么局部变量有这些限制。 首先,有一把钥匙 实例和本地变量在幕后实现方式上的差异 变量存储在堆中,而本地变量存储在堆栈中 直接访问本地变量,并在线程中使用 lambda,然后使用 Lambda 可以在分配变量的线程拥有 因此,Java 实现了对一个自由本地变量的访问,作为对其副本的访问 而不是访问原始变量。如果局部变量是 只分配给一次ーー因此有限制。 其次,这种限制也不鼓励典型的命令式编程模式(正如我们 在后面的章节中进行解释,防止容易的并行化) ,这会使外部变量发生变化。

是的,您可以更改实例的 成员变量,但是您可以像处理 变量一样更改实例本身。

就像上面提到的:

    class Car {
public String name;
}


public void testLocal() {
int theLocal = 6;
Car bmw = new Car();
bmw.name = "BMW";
Stream.iterate(0, i -> i + 2).limit(2)
.forEach(i -> {
//            bmw = new Car(); // LINE - 1;
bmw.name = "BMW NEW"; // LINE - 2;
System.out.println("Testing local variables: " + (theLocal + i));


});
// have to comment this to ensure it's `effectively final`;
//        theLocal = 2;
}

限制 局部变量局部变量的基本原则是关于 数据和计算有效性

如果通过第二个线程计算的 lambda 具有变换局部变量的能力。即使能够从不同的线程读取可变局部变量的值,也需要使用 同步反复无常来避免读取过时的数据。

但我们知道 主要目的

在这些不同的原因中,Java 平台最迫切的原因是它们使得在 多线程上分发集合处理变得更加容易。

与局部变量不同,局部 例子可以突变,因为它是全局 分享。我们可以通过 堆和堆栈差异堆和堆栈差异更好地理解这一点:

无论何时创建对象,它总是存储在堆空间中,堆栈内存包含对它的引用。堆栈内存只包含本地基元变量和堆空间中对象的引用变量。

总而言之,我认为有两点很重要:

  1. 制作 例子 实际上是最终决定真的很难,这可能会造成很多无意义的负担(想象一下深度嵌套的类) ;

  2. 实例本身已经是全局共享的,lambda 也可以在线程之间共享,所以它们可以正确地协同工作,因为我们知道我们正在处理 变异,并希望传递这个突变;

这里的平衡点是明确的: 如果你知道你在做什么,你可以做到 很容易,但如果没有那么 默认限制将有助于避免 阴险错误。

附言。如果 实例突变中需要 同步,你可以直接使用 蒸汽压缩法蒸汽压缩法或者如果 实例突变中存在依赖问题,你仍然可以在 功能中使用 thenApply或者 thenCompose,而 mapping或者类似的方法。

为未来的访客提供一些概念:

基本上这一切都可以归结为 编译器应该能够确定地告诉 lambda 表达式主体不在变量的过期副本上工作

对于局部变量,编译器无法确定 lambda 表达式主体是否正在处理变量的陈旧副本,除非该变量是 final 或者实际上是 final,因此局部变量应该是 final 或者实际上是 final。

现在,在实例字段的情况下,当你访问 lambda 表达式中的一个实例字段时,编译器会在该变量的访问中附加一个 this(如果你没有显式地这样做的话) ,而且因为 this实际上是 final,所以编译器要确保 lambda 表达式主体总是有变量的最新副本(请注意,多线程现在不在讨论的范围内)。因此,在 case 实例字段中,编译器可以告诉 lambda body 有最新的实例变量副本,所以实例变量不必是 final 或者实际上是 final。请参阅下面 Oracle 幻灯片的屏幕截图:

enter image description here

另外,请注意,如果您正在访问 lambda 表达式中的一个实例字段,并且该字段正在多线程环境中执行,那么您可能会遇到问题。

首先,本地变量和实例变量在幕后的实现方式有一个关键的区别。实例变量存储在堆中,而本地变量存储在堆栈中。 如果 lambda 可以直接访问本地变量,并且 lambda 在线程中使用,那么使用 lambda 的线程可以在分配变量的线程释放该变量之后尝试访问该变量。

简而言之: 为了确保另一个线程不会覆盖原始值,最好提供对复制变量的访问,而不是对原始变量的访问。