所有最终变量都被匿名类捕获吗?
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)this
和 obj1
(ALOAD 1) 创建新的 test$1
所以我不知道,您是如何得出将obj2
传递给匿名类实例的结论,但这是完全错误的。 IDK如果它是编译器依赖的,但至于其他所说的,也不是不可能的。
【讨论】:
回答您的问题“我是如何得出结论的”...我以为我很久以前在某个地方读过它,但看起来我记错了。我设想一个编译器在第一次开始处理内部类时创建一个隐藏对象,该对象引用外部this
并引用所有 final
变量和参数。也许我创造了一个心理画面来帮助我理解内部类发生了什么,然后把它与我认为我读到的东西混淆了?我不知道。【参考方案4】:
obj2 将被垃圾回收,因为它没有引用它。只要事件处于活动状态,obj1 就不会被垃圾回收,因为即使您创建了一个匿名类,您也已经创建了对 obj1 的直接引用。
final 唯一要做的就是你不能重新定义值,它不能保护对象免受垃圾收集器的影响
【讨论】:
如果您不理解 OP,为什么要在已经由 4 个不同的人回答的情况下发布此问题的答案。 OP 的意思是他认为即使obj2
没有在匿名类中使用,它仍然持有对它的引用(顺便说一句,这是错误的)。它与最终或非声明无关。
我根据标题回答,只要未实例化外部类,op post 中的 obj2 就没有外部引用
您没有抓住重点,如果您根据标题回答,恕我直言,这仍然是题外话。以上是关于所有最终变量都被匿名类捕获吗?的主要内容,如果未能解决你的问题,请参考以下文章