【问题标题】:Why a non-final "local" variable cannot be used inside an inner class, and instead a non-final field of the enclosing class can?为什么非最终“局部”变量不能在内部类中使用,而封闭类的非最终字段可以?
【发布时间】:2011-08-13 16:42:16
【问题描述】:

在编译器错误Cannot refer to a non-final variable message inside an inner class defined in a different method 上有一些关于 Stack Overflow 的主题,解决方案是“将其声明为最终的,你就完成了”,但是我想通过这个 理论上的问题检查这段代码无法编译的逻辑原因是什么:

private void updateStatus(String message) {
    Runnable doUpdateStatus = new Runnable() {
         public void run() {
             /* do something with message */
         }
    }
    /* do something with doUpdateStatus, like SwingUtilities.invokeLater() */
}

(解决方案:将message 声明为最终结果)而这个是:

private String enclosingClassField;
private void updateStatus() {
    Runnable doUpdateStatus = new Runnable() {
         public void run() {
             /* do something with enclosingClassField */
         }
    }
    /* do something with doUpdateStatus, like SwingUtilities.invokeLater() */
}

我真的很困惑。 enclosingClassField 不是最终的,它可以每次更改很多次,而 updateStatus 的可怜的 message 参数只能在其方法体内更改,而是被编译器指责;)

即使是编译器错误也会误导我。 Cannot refer to a non-final variable message inside an inner class defined in a different method和什么不同? message 不是定义在和内部类相同的方法中吗? enclosingClassField 不是在方法之外定义的吗?嗯……

有人可以指出我对此事的正确解释吗?谢谢。

【问题讨论】:

    标签: java


    【解决方案1】:

    原因是Java不支持closures。没有 JVM 命令可以从方法外部访问局部变量,而类的字段可以从任何地方轻松访问。

    因此,当您在内部类中使用final 局部变量时,编译器实际上将该变量的值传递给内部类的构造函数。显然,它不适用于非final 变量,因为它们的值在内部类构建后会发生变化。

    包含类的字段没有这个问题,因为编译器隐式地将包含类的引用传递给内部类的构造函数,因此您可以像访问任何其他字段一样以正常方式访问其字段类。

    【讨论】:

      【解决方案2】:

      区别在于局部(方法)变量与类成员变量。成员变量存在于封闭对象的生命周期中,因此它可以被内部类实例引用。然而,局部变量仅在方法调用期间存在,并且编译器以不同的方式处理,因为它的隐式副本作为内部类的成员生成。如果不将局部变量声明为 final,则可能会对其进行更改,从而导致细微的错误,因为内部类仍然引用该变量的原始值。

      更新:The Java Specialists' Newsletter #25 对此进行了更详细的讨论。

      即使是编译器错误也会误导我。 Cannot refer to a non-final variable message inside an inner class defined in a different method有什么不同?

      我相信来自内部类的run 方法。

      【讨论】:

      • 因此 Java 可以复制变量的值而不会受到此编译器错误的影响,但它会强制您将变量声明为 final 以告诉您“嘿,记住它被复制到内部类,所以如果你以后可以改变它,那么你会得到一个严重的不一致。所以你不能改变它,我们很清楚这一点。”
      • @Giacomo,没错。设计人员试图尽量减少开发人员可以在脚上开枪的方式的数量:-) 这反过来可能会导致编译器的不直观(乍一看)行为,但恕我直言,它仍然比运行程序的不直观行为要好得多.
      【解决方案3】:

      您使用的值必须是最终的,但最终引用的非最终字段可以更改。注意:this 是隐含的最终引用。你不能改变它。

      private String enclosingClassField;
      private void updateStatus() {
          final MutableClass ms = new MutableClass(1, 2);
          Runnable doUpdateStatus = new Runnable() {
               public void run() {
                   // you can use `EnclosingClass.this` because its is always final
                   EnclosingClass.this.enclosingClassField = "";
                   // shorthand for the previous line.
                   enclosingClassField = "";
                   // you cannot change `ms`, but you can change its mutable fields.
                   ms.x = 3;
               }
          }
          /* do something with doUpdateStatus, like SwingUtilities.invokeLater() */
      }
      

      【讨论】:

      • 是的,我知道,它与 C++ const 完全不同。谢谢。
      • final 是一个浅常量,而 C++ const 是整个对象及其引用的任何内容。在 Java 中,即使 enums 也可以是可变的 ;)
      【解决方案4】:

      三类事物:实例变量、局部变量和对象:

      ■ 实例变量和对象位于堆上。 ■ 局部变量存在于堆栈中。

      内部类对象不能使用定义局部内部类的方法的局部变量。

      因为使用方法的局部变量是方法的局部变量保存在堆栈上,方法一结束就丢失。

      但是即使在方法结束之后,本地内部类对象可能仍然在堆上存活。方法局部内部类仍然可以使用标记为final的局部变量。

      最终变量 JVM 将这些作为常量,因为它们在启动后不会改变。当内部类尝试访问它们时编译器会将该变量的副本创建到堆中并在内部类中创建一个合成字段,因此即使方法执行结束它也可以访问,因为内部类有它自己的副本

      合成字段在源代码中实际上不存在,但编译器在一些内部类中创建这些字段以使这些字段可访问。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-03-02
        • 2019-08-19
        相关资源
        最近更新 更多