local inner 类 是否在定义它们的范围内维护所有局部变量的副本?

Do local inner classes maintain a copy of all local variables in the scope that they are defined in?

做局部内部classes(我相信包括匿名classes)维护在这些局部内部classes的方法范围内定义的所有变量的副本定义在?

这是因为如果局部内部 classes 正在使用局部变量并且方法超出范围而内部 class 对象仍然存在,则该对象将使用某些东西不再存在。

但是这个说法是真的吗?官方文档中有提到它吗?

这是 this answer 的后续问题。我在那里发表评论但没有得到回复,我认为这也是一个不同的问题,所以在这里提问。

不,局部内部 类 不 "keep a copy",但它们可以引用 final 或者,因为 , effectively final 方法中的局部变量。

更阴险的是,他们保留对 实例 的引用,这可能会导致内存泄漏。

Do local inner classes maintain a copy of all local variables in the scope that they are defined in?

"Copy"是一个有歧义的词。
但是必须进行某种复制才能处理两个作用域,这两个作用域在实例化后各走各的路。

首先,局部内部 classes 只能引用封闭范围的变量,如果那些是 "final or effectively final",正如 javac 所说的那样。
尝试使用其值在局部内部 class 声明后发生变化的变量将产生如下错误消息:

Test.java:13: error: local variables referenced from an inner class must be final or effectively final
                System.out.println(i);
                               ^

因为我们只有最终变量,所以可以毫不费力地复制原始值。

至于对象,因为它们 are actually pointers in Java,我们只复制指针而不是整个对象,就像在任何其他对象变量赋值中一样。
这样做会为对象创建一个额外的 "reference",这将阻止垃圾收集器在原始变量超出范围后清理它,只要我们的内部 class 仍然是 运行 .

Is it mentioned somewhere in official docs?

我不这么认为。我唯一能找到的是 §8.1.3 of the Java Language Specification,这是非常模糊的:

When an inner class (whose declaration does not occur in a static context) refers to an instance variable that is a member of a lexically enclosing class, the variable of the corresponding lexically enclosing instance is used.

(而且我不确定 static 方法中的声明是否符合 "static context" 的定义。)

但是,您可以通过使用 javap -c -private ....

反汇编 .class 文件来检查 javac 如何处理此问题

(假设从这里开始 Java 8)
给定以下代码:

class Test
{
    public static void main(String[] args)
    {
        int i = 42;
        String s = "abc";
        Object o = new Object();
        java.io.InputStream in = null;
        Runnable r = new Runnable()
        {
            public void run()
            {
                System.out.println(i);
                System.out.println(s);
                System.out.println(o);
                System.out.println(in);
            }
        };
    }
}

所以我们在外部作用域中有一个 int、一个 String、一个 Object 和一个 InputStream,从内部作用域引用。

调用 javac Main.java 会创建两个文件:Test.classTest.class
Test.class 并不太有趣,但是 Test.class 揭示了这些变量是如何存储和稍后访问的:

bash$ javap -c -private 'Test.class'
Compiled from "Test.java"
final class Test implements java.lang.Runnable {
  final int val$i;

  final java.lang.String val$s;

  final java.lang.Object val$o;

  final java.io.InputStream val$in;

  Test(int, java.lang.String, java.lang.Object, java.io.InputStream);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #1                  // Field val$i:I
       5: aload_0
       6: aload_2
       7: putfield      #2                  // Field val$s:Ljava/lang/String;
      10: aload_0
      11: aload_3
      12: putfield      #3                  // Field val$o:Ljava/lang/Object;
      15: aload_0
      16: aload         4
      18: putfield      #4                  // Field val$in:Ljava/io/InputStream;
      21: aload_0
      22: invokespecial #5                  // Method java/lang/Object."<init>":()V
      25: return

  public void run();
    Code:
       0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #1                  // Field val$i:I
       7: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      10: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      13: aload_0
      14: getfield      #2                  // Field val$s:Ljava/lang/String;
      17: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_0
      24: getfield      #3                  // Field val$o:Ljava/lang/Object;
      27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      30: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      33: aload_0
      34: getfield      #4                  // Field val$in:Ljava/io/InputStream;
      37: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      40: return
}

所以基本上 javac 只是在内部 class 为外部作用域的每个引用变量创建一个 final 字段和相应的构造函数参数。

我不知道其他编译器是怎么做到的,但是

  • 我还没有找到这方面的规范。
  • 您可以使用您最喜欢的 Java 反汇编程序检查它们每个人是如何做到的。

为了清楚起见

内部(非静态)class 对象可以访问外部 classes this。在对象上有一些隐式字段,thisOuter.this、OuterOuter.this 就嵌套而言。

class Outer {

    int outerField;

    class Inner {
         String innerField;
         ... innerField ... outerField
         ... this.innerField
         ... Outer.this
    }

    ... new Inner();
}

Outer outer = new Outer();
Inner inner = outer.new Inner();

优点:内部classes允许创建一个容器class提供项目,每个项目都可以使用外部this访问容器场.

缺点:如果将这样的内部class对象序列化到文件,它也会序列化外部对象,所以行为可能很奇怪。

(答案应该是显而易见的。)

这里发生了几件事:

  • JLS(Java 语言标准)关注语义。从语义上讲,没有复制正在进行(或者更确切地说,复制的效果是无法区分的)。所以它什么也没说。
  • 编译器关注实现。创建变量的 under-the-hood 副本是实现 local-variable 捕获语义的一种非常简单的方法(给定 non-mutation)。
  • JLS 的作者当然知道以上几点,因此几乎可以肯定地策划了相应的语义(特别是 final/effectively-final 周围的约束)。

所以重新审视你的标题问题:

Do local inner classes maintain a copy of all local variables in the scope that they are defined in?

答案是 JLS 对此什么也没说,在实际的编译器实现中,它可能只是 final/effectively-final 变量的副本。