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

Posted

技术标签:

【中文标题】所有最终变量都被匿名类捕获吗?【英文标题】:Are all final variables captured by anonymous classes? 【发布时间】:2021-06-08 07:32:49 【问题描述】:

我以为我知道这个问题的答案,但经过一个小时左右的搜索,我找不到任何确认。

在这段代码中:

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 在对象存在时不能被垃圾回收?我的印象是all可见final(或实际上是最终的)局部变量和参数被捕获,因此只要对象还活着就不能被GC'ed,但我可以'找不到任何说法。

它是否依赖于实现?

JLS 中有一个部分可以回答这个问题吗?我无法在那里找到答案。

【问题讨论】:

你怎么知道obj2 绑定到callback$x?你在字节码中见过吗? "是否依赖于实现?"从技术上讲,是的。匿名类没有理由捕获obj2,但没有理由不能。 我认为它不会捕获obj2 的一个很好的理由当然是你可以在一个方法中声明多个匿名类:其中一个类可能只引用obj1,而另一个可能只提到obj2。两个类都捕获这两个变量是不明智的。 您可以使用反射或调试器进行检查。 【参考方案1】:

语言规范几乎没有说明匿名类应如何从其封闭范围中捕获变量。

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

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

(Anonymous classes are inner classes)

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

我认为由此推断实现不需要捕获内部类中未引用的变量是合理的;但并不是说他们不能。

【讨论】:

【参考方案2】:

仅捕获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 必须是最终的,所以你永远不会对这种行为感到困惑。

【讨论】:

值得一提的是,自 Java 8 以来,必须明确声明 final 的限制已被取消。 JLS 现在谈论“有效final”变量。 这不仅仅是逻辑上的,它几乎与编译器所做的完全一样(Java 8 和 11),只是字段名称不同 [:-) 注意this的封闭实例无论是否被使用都会被捕获。 @CarlosHeuberger 并且类本身的命名不同,使用了 Java 语法不允许的名称(例如 Outer$1)。【参考方案3】:

我对你的陈述感到好奇和惊讶(为什么编译器会做这样的事情???),我不得不自己检查一下。所以我做了这样的简单例子

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 - 使用 idx 1 (ASTORE 1) 存储第一个最终结果 L1 - 使用 idx 2 存储第二个 final(未在 anon 类中使用)(ASTORE 2) L2 - 使用参数 (ALOAD 0) thisobj1 (ALOAD 1) 创建新的 test$1

所以我不知道,您是如何得出将obj2 传递给匿名类实例的结论,但这是完全错误的。 IDK如果它是编译器依赖的,但至于其他所说的,也不是不可能的。

【讨论】:

回答您的问题“我是如何得出结论的”...我以为我很久以前在某个地方读过它,但看起来我记错了。我设想一个编译器在第一次开始处理内部类时创建一个隐藏对象,该对象引用外部 this 并引用所有 final 变量和参数。也许我创造了一个心理画面来帮助我理解内部类发生了什么,然后把它与我认为我读到的东西混淆了?我不知道。【参考方案4】:

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

final 唯一要做的就是你不能重新定义值,它不能保护对象免受垃圾收集器的影响

【讨论】:

如果您不理解 OP,为什么要在已经由 4 个不同的人回答的情况下发布此问题的答案。 OP 的意思是他认为即使obj2 没有在匿名类中使用,它仍然持有对它的引用(顺便说一句,这是错误的)。它与最终或非声明无关。 我根据标题回答,只要未实例化外部类,op post 中的 obj2 就没有外部引用 您没有抓住重点,如果您根据标题回答,恕我直言,这仍然是题外话。

以上是关于所有最终变量都被匿名类捕获吗?的主要内容,如果未能解决你的问题,请参考以下文章

java中,匿名内部类可以使用外部类的成员变量吗

Lambda 表达式和变量捕获

我的学习之路_第三章_匿名内部类

匿名内部类详解

匿名内部类

“有效最终”的变量是啥意思? [复制]