所有的最终变量都会被匿名类捕获吗?

17

我原以为我知道这个答案,但是搜了一个小时左右也没找到确认的资料。

在这段代码中:

public class Outer {

    // other code

    private void method1() {
        final SomeObject obj1 = new SomeObject(...);
        final SomeObject obj2 = new SomeObject(...);
        someManager.registerCallback(new SomeCallbackClass() {
            @Override
            public void onEvent() {
                 System.out.println(obj1.getName());
            }
        });
    }
}

假设registerCallback将其参数保存在某个地方,以便匿名子类的对象可以存在一段时间。显然,该对象必须维护对obj1的引用,以便在调用onEvent时正常工作。

但是,考虑到该对象不使用obj2,它是否仍然保持对obj2的引用,以使得obj2在对象存活期间无法被垃圾回收?我认为所有可见的final(或有效的 final)本地变量和参数都会被捕获,因此只要对象存活就不能被GC回收,但我找不到任何一个说法能证实这一点。

这是实现相关的吗?

JLS中是否有解答这个问题的章节?我在那里找不到答案。


你怎么知道 obj2 绑定到了 callback$x?你在字节码中看到了吗? - Antoniossss
1
"这是否取决于实现?"技术上是的。匿名类没有必要捕获obj2,但是也没有任何理由不这样做。" - Andy Turner
我认为它不会默认捕获obj2的一个很好的理由是,你可以在一个方法中声明多个匿名类:其中一个类可能只引用obj1,而另一个类可能只引用obj2。让两个类都捕获两个变量是不明智的。 - Andy Turner
你可以使用反射或调试器来检查这个。 - Peter Lawrey
4个回答

12

语言规范对于匿名类如何从封闭作用域中捕获变量几乎没有详细说明。

我能找到的唯一特别相关的语言规范部分是JLS Sec 8.1.3:

在内部类中使用但未声明的任何局部变量、形式参数或异常参数必须被声明为 final 或有效 final(§4.12.4),否则会在使用时产生编译时错误。

匿名类是内部类

它并没有指定匿名类应该捕获哪些变量,或者捕获应该如何实现。

我认为可以合理地推断出,实现不需要捕获内部类中未引用的变量。但它并没有说不能这样做。


12

仅捕获obj1

从逻辑上讲,匿名类实现为普通类,就像这样:

class Anonymous1 extends SomeCallbackClass {
    private final Outer _outer;
    private final SomeObject obj1;
    Anonymous1(Outer _outer, SomeObject obj1) {
        this._outer = _outer;
        this.obj1 = obj1;
    }
    @Override
    public void onEvent() {
         System.out.println(this.obj1.getName());
    }
});

请注意,匿名类始终是内部类,因此即使不需要,它仍将保留对外部类的引用。我不知道编译器的后续版本是否已经进行了优化,但我认为没有。这是可能导致内存泄漏的潜在原因。

使用方法如下:

someManager.registerCallback(new Anonymous1(this, obj1));

正如您所见,obj1的引用值被复制(按值传递)。

从技术上讲,obj1没有必要是final,无论是声明为final还是有效地final(Java 8+),除非您改变了值并且副本不会发生变化,导致错误,因为预期值会发生变化,考虑到复制是一个隐藏的操作。为了防止程序员混淆,他们决定obj1必须是final,这样您就永远不会对该行为感到困惑。


2
值得一提的是,自Java 8以来,不必显式声明 final 的限制已被取消。JLS现在谈论的是"实际上是final"的变量。 - Boris the Spider
这不仅是 逻辑上 的,几乎就是编译器所做的事情(Java 8和11),只是字段的命名不同而已 [:-) - user85421
3
注意:无论是否使用,封闭实例的 this 都会被捕获。 - Peter Lawrey
@Andreas 我忘了那个,但事实上Java语言规范允许... $是一个"Java字母",也就是说,它作为标识符的一部分是有效的,只是不建议使用: "美元符号应仅在机械生成的源代码中使用,或者很少用于访问旧系统上的现有名称"。 有趣的类名,只有$。 - user85421
@CarlosHeuberger 实际上,类名是 1,这不是一个有效的标识符。Outer$ 前缀是 限定 名称的一部分,因为匿名类是嵌套类。 - Andreas
显示剩余4条评论

2

我对你的语句感到好奇和惊讶(编译器为什么要这样做?),所以我必须自己检查一下。因此,我做了一个简单的例子:

public class test {
    private static Object holder;

    private void method1() {
        final Object obj1 = new Object();
        final Object obj2 = new Object();
        holder = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(obj1);
            }
        };
    }
}

最终得到以下method1的字节码:

 private method1()V
   L0
    LINENUMBER 8 L0
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 9 L1
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 10 L2
    NEW test$1
    DUP
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL test$1.<init> (Ltest;Ljava/lang/Object;)V
    PUTSTATIC test.holder : Ljava/lang/Object;

意思是:

  • L0 - 使用索引1存储第一个final变量(ASTORE 1)
  • L1 - 使用索引2存储第二个final变量(在匿名类中未使用)(ASTORE 2)
  • L2 - 使用参数(ALOAD 0)thisobj1(ALOAD 1)创建新的test$1

所以我不知道你是怎么得出obj2被传递给匿名类实例的结论的,但这是错误的。我不知道它是否与编译器有关,但据其他人所述,这并非不可能。


回答你的问题“我是如何得出结论的”... 我认为我很久以前在某个地方读过,但看起来我记错了。我设想编译器在首次处理内部类时创建一个隐藏对象,其中包含对外部this和所有final变量和参数的引用。也许我创造了一个心理图像来帮助我理解内部类的运行方式,后来将其与我认为读到的某些内容混淆了?我不知道。 - ajb

0

由于obj2没有引用,所以它将被垃圾回收。只要事件处于活动状态,即使您创建了一个匿名类,也已经创建了对obj1的直接引用,因此不会对obj1进行垃圾回收。

final唯一作用是您不能重新定义值,但它无法保护对象免受垃圾收集器的影响。


1
如果你不理解提问者的问题,为什么要在已经有4个不同人回答的情况下回答这个问题呢? - SamHoque
OP 的意思是,他认为即使在匿名类中没有使用 obj2,它仍然保留对它的引用(顺便说一下,这是错误的)。这与是否为 final 声明无关。 - Antoniossss
我根据标题回答,op帖子中的obj2只要外部类没有被实例化就没有外部引用。 - Nertan Lucian
1
你没有理解重点,如果你只是根据标题回答的话,在我看来仍然是离题的。 - Antoniossss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接