变量在 Lambda 中的捕获

huangapple go评论65阅读模式
英文:

Variable capture in Lambda

问题

我想不通为什么在lambda表达式中捕获的变量是final的,或者在实质上是final的。我查看了这个问题,但实际上并没有得到答案。

什么是变量捕获?

在寻找解决方案解决我的问题时,我读到这些变量之所以是final的,是因为并发问题。但是对于这种情况,为什么我们不能在lambda中用“可重入锁”对象锁定任务代码呢?

public class Lambda {

  private int instance = 0;

  public void m(int i, String s, Integer integer, Employee employee) {

    ActionListener actionListener = (event) -> {
      System.out.println(i);
      System.out.println(s);
      System.out.println(integer);
      System.out.println(employee.getI());
      this.instance++;
      employee.setI(4);
      integer++; // 错误
      s = "fghj"; // 错误
      i++; // 错误
    };
  }

}

在这段特定的代码中,我想知道为什么最后三个语句会出错,以及为什么我们可以修改Employee,因为它是一个局部变量。(Employee只是一个具有int i的getter和setter的类。)

此外,我想知道为什么我们也可以修改this.instance

我会详细回答您提到的所有事实。

英文:

I can't think why the captured variables are final or effectively final in lambda expressions. I looked over this question and really quite didn't get the answer.

What is this variable capture?

As I searched solutions for my problem, I read that these variables are final because of concurrency problems. But for such situation why can't we lock the task code in the lambda with a reentrant lock object.

public class Lambda {

  private int instance=0;

  public void m(int i,String s,Integer integer,Employee employee) {

    ActionListener actionListener = (event) -> {
      System.out.println(i);
      System.out.println(s);
      System.out.println(integer);
      System.out.println(employee.getI());
      this.instance++;
      employee.setI(4);
      integer++;//error
      s="fghj";//error
      i++;//error
    };
  }

}

In this particular code I want know the reasons why the last three statements gives an error, and why do we get to mutate Employee since it's a local variable.(Employee is just a class with getters and setters ofint i.)

Also i like to know why we can mutate this.instance too.

I appreciate a full detailed answer on all facts I mentioned above.

答案1

得分: 9

> 我读到这些变量是final的,因为存在并发问题。

错误,这与并发无关,而是与Lambda(和匿名类)*“捕获”*变量值有关。

> 我想知道为什么最后三个语句会出错

因为它们是捕获的,所以它们必须是有效final

你真的不需要知道为什么内部需要这样,只需接受你需要遵守这个规则。

> 我想知道为什么我们可以改变this.instance

因为代码没有捕获instance,它捕获this,而this是隐式final的。


<h3>原因</h3>

Lambda主要是匿名类的一种语法糖。这不完全准确,但在这个解释中,足够准确,而且对于匿名类来说更容易理解。

首先要明白,在JVM中没有匿名类。实际上,也没有Lambda表达式,但这是另一回事。

然而,由于Java(语言)有匿名类,但JVM没有,编译器必须伪造它,将匿名类转换为内部类。(顺便说一句:JVM中也不存在内部类,所以编译器也必须伪造它。)

让我们通过示例来理解这一点。假设我们有这段代码:

// 作为匿名类
int i = 0;
Runnable run = new Runnable() {
    @Override
    public void run() {
        System.out.println(i);
    }
}

// 作为Lambda表达式:
int i = 0;
Runnable run = () -> System.out.println(i);

对于匿名类,编译器将生成一个类似于以下的类:

final class Anon_1 implements Runnable {
    private final int i;
    Anon_1(int i) {
        this.i = i;
    }
    @Override
    public void run() {
        System.out.println(i);
    }
}

然后将代码编译为:

int i = 0;
Runnable run = new Anon_1(i);

这就是捕获的工作原理,通过复制“捕获”的变量的值。

变量根本没有被捕获,只有值被捕获,因为Java在构造函数调用中是按值传递的。

现在你可以争论,为什么i应该是有效final。当然,局部变量i和字段i现在是分开的,但它们可以分别修改。

但是有一个原因,而且是一个非常好的原因。i已经被复制并且是独立的,这是完全隐藏的,是一个实现细节。程序员经常会忘记这一点,并认为它们是相同的,这会导致大量失败的代码以及为了提醒这一点而浪费的许多调试小时。

为了代码清晰度,必须使局部变量有效final,以便(内部)变量根本没有被捕获,对运行代码没有任何影响,就好像匿名类中的i与外部的i相同的,因为这是Java语言定义的,尽管JVM无法做到这一点。

为了使其看起来像是这样,局部变量必须是有效final的,这样(内部)变量根本没有被捕获对运行代码没有任何影响。

英文:

> I read that these variables are final because of concurrency problems.

Wrong, this has nothing to do with concurrency, it's all about how lambdas (and anonymous classes) "capture" variable values.

> I want know the reasons why the last three statements gives an error

Because they are captures, so they must be effectively final.

You really don't need to know why the internals require this, just accept the fact that you need to adhere to that rule.

> i like to know why we can mutate this.instance

Because the code doesn't capture instance, it captures this, and this is implicitly final.


<h3>Reason Why</h3>

A lambda is mostly syntactic sugar for an anonymous class. That's not really true, but for the purpose of this explanation, it's true enough, and the explanation is easier to understand for an anonymous class.

First understand, there is no such thing as an anonymous class in the JVM. Actually, there is no such thing as a lambda expression either, but that's a different story.

However, since Java (the language) has anonymous classes, but the JVM doesn't, the compiler has to fake it, by converting the anonymous class into an inner class. (FYI: Inner classes don't exist in the JVM either, so the compiler has to fake that too.)

Let's do this by example. Say we have this code:

// As anonymous class
int i = 0;
Runnable run = new Runnable() {
    @Override
    public void run() {
        System.out.println(i);
    }
}

// As lambda expression:
int i = 0;
Runnable run = () -&gt; System.out.println(i);

For the anonymous class, the compiler will generate a class like this:

final class Anon_1 implements Runnable {
    private final int i;
    Anon_1(int i) {
        this.i = i;
    }
    @Override
    public void run() {
        System.out.println(i);
    }
}

and then compile the code to:

int i = 0;
Runnable run = new Anon_1(i);

That's how capture works, by copying the value of the "captured" variable.

The variable isn't captured at all, the value is, because Java is pass-by-value in the constructor call.

Now you can argue, that there is no reason why i should be effectively final. Sure, the local variable i and the field i are now separate, but they could be separately modified.

But there is a reason, and it's a really good reason. The fact that i has been copied, and is separate, is entire hidden, and is an implementation detail. Programmers would constantly forget that, and think they are the same, which would lead to lots of failed code, and many wasted hours of debugging to be reminded of that.

For code clarity, it must be as-if the i local variable was captured, and that the i in the anonymous class is the same as the i outside, because that is what the Java language defines it to be, even though the JVM can't do that.

To make it appear like that, the local variable MUST be effectively final, so the fact that (internally) the variable wasn't captured at all, makes no difference to the running code.

huangapple
  • 本文由 发表于 2020年4月7日 16:02:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/61075400.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定