【问题标题】:Why does java allow class level variables to be reassigned in anonymous inner class, whereas same is not allowed for local variables [duplicate]为什么java允许在匿名内部类中重新分配类级变量,而局部变量则不允许[重复]
【发布时间】:2019-05-10 14:24:29
【问题描述】:

这个问题类似于Lambdas: local variables need final, instance variables don't,但唯一的区别是这个问题即使没有 lambda 表达式也有效,即即使在 Java7 上也有效。

这是下面的代码sn-p。

public class MyClass {

    Integer globalInteger = new Integer(1);

    public void someMethod() {

        Integer localInt = new Integer(2);

        Runnable runnable = new Runnable() {

            @Override
            public void run() {

                globalInteger = new Integer(11);//no error
                localInt =  new Integer(22);//error here

            }
        };      
    }
}

我可以为 globalInteger 重新分配一个新值,但不能给 localInteger。为什么会有这种差异?

【问题讨论】:

  • 考虑一个实例字段等同于this.field = newValue 这就像someLocalVar.field = newValue 一样工作。但考虑包含一段说明性的代码,这样更容易获得真正的答案。
  • 请提供示例代码。
  • 我已经添加了代码sn-p
  • globalInteger = 11;this.globalInteger = 11; 的简写,但 localInt 只是值的副本,不能更改原始值。

标签: java lambda anonymous-inner-class


【解决方案1】:

要了解为什么允许非局部变量更改,我们首先需要了解为什么不允许局部变量。这是因为局部变量存储在堆栈中(哪些实例(或静态)变量没有)。

堆栈变量的问题在于,一旦它们的包含方法返回,它们就会消失。但是,您的匿名类的实例可能会比这更长。因此,如果访问局部变量是天真的实现的,则在返回方法之后从内部类内部使用局部变量将访问不再存在的堆栈帧上的变量。这将导致崩溃、异常或未定义的行为,具体取决于确切的实现。由于这显然很糟糕,因此访问局部变量是通过复制来实现的。也就是说,类使用的所有局部变量(包括特殊变量this)都被复制到匿名对象中。因此,当内部类的方法访问局部变量x 时,它实际上并没有访问该局部变量。它正在访问存储在对象中的它的副本。

但是,如果在创建对象后局部变量发生了更改,或者对象的方法更改了变量,会发生什么情况呢?好吧,前者会导致局部变量改变,但不会改变对象中的副本,而后者会改变副本,但不会改变原始变量。因此,无论哪种方式,变量的两个版本都将不再相同,这对于任何不了解复制正在进行的程序员来说都是非常违反直觉的。所以为了避免这个问题,只有当局部变量的值从未改变时,你才被允许访问它们。

不需要复制实例变量,因为它们不会消失,直到它们的包含对象被垃圾回收(静态变量永远不会消失) - 因为匿名对象将包含对外部 this 的引用,所以这赢了在匿名对象也被垃圾收集之前不会发生。因此,由于它们没有被复制,因此修改它们不会导致任何问题,也没有理由禁止它。

【讨论】:

  • 在调用匿名内部类的实例化后,我尝试 sysout 的 globalInteger 值,但它不反映匿名类中所做的更改。所以我想即使是全局变量也会被复制。所以问题仍然存在:S
  • @t22 你能展示你试过的代码吗?静态变量(我假设这就是您所说的“全局”,对吗?)和实例变量绝对不会被复制。
  • @t22 ideone.com/2T4pdf 看到了吗?值肯定会改变。
  • ideone.com/93cltw。它没有:(
  • @t22 在您的示例中,您从未真正调用过method()
【解决方案2】:

因为 JVM 没有机器指令来分配位于与当前堆栈帧不同的任何堆栈帧的变量。

【讨论】:

    【解决方案3】:

    因为 lambda 函数不是类的一部分。

    想想下面的代码(你的小改动):

    public Runnable func() {
        Integer localInt = new Integer(2);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                globalInteger = new Integer(11);//no error
                localInt =  new Integer(22);//error here
            }
        };
        return runnable;
    }
    
    //Somewhere in the code:
    Runnable r = func();
    r.run(); // At this point localInt is not defined.
    

    【讨论】:

    • 这个解释略过了一个事实,即以不修改它的方式使用localInt 是完全合法的。如果没有定义,那么阅读它就和写它一样糟糕,不是吗?
    • @sepp2k - 不完全是,这与 lambda 的标题如何工作有关。它允许您读取 Effectively final 的局部变量,这意味着您无法在 lambda 函数内部或外部更改它们。然后,当创建 lambda 函数时,它实际上将值复制到 lambda 函数的验证中,稍后您实际上读取的是副本而不是原始参数。欲了解更多信息,您可以阅读:javahabit.com/2016/06/16/…
    【解决方案4】:

    编译器告诉你内部类中的变量必须是最终或有效最终的错误。这是由于 Java 的 8 闭包以及 JVM 如何捕获引用。限制是在 lambda 主体中捕获的引用必须是最终的(不可重新分配),并且编译器需要确保它不引用局部变量的副本。

    因此,如果您访问实例变量,则您的 lambda 确实引用了周围类的 this 实例,这是有效的最终(不变引用)。此外,如果您使用包装类或数组,编译器错误也会消失。

    【讨论】:

      猜你喜欢
      • 2020-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-19
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-12
      相关资源
      最近更新 更多