匿名类和父类之间的循环依赖是否是错误的?

25

我有以下的代码片段:

public class Example {

private Integer threshold;

private Map<String, Progress> history;

protected void activate(ComponentContext ctx) {
    this.history = Collections.synchronizedMap(new LinkedHashMap<String, Progress>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Progress> entry) {
            return size() > threshold;
        }
    });
  }
}

匿名LinkedHashMap类和Example类之间存在循环依赖关系。这样可以吗?为什么不行?它会被垃圾收集器良好回收吗?


一个非常经典的匿名内部类使用案例。我可能对循环引用不太容忍,但这从来没有困扰过我(有时候没有这个引用会很难处理)。 - Chop
5个回答

22

这样做没问题吗?

完全没有问题。

threshold是一个字段,因此可以在匿名类内部引用它而不会出现任何问题。(如果threshold是一个局部变量,那么它就必须是(有效地)final。)

类之间的循环依赖是很常见的,当依赖图比较小(如此例)时,不会出现任何问题。你的LinkedHashMap是一个匿名类并不影响。

它会被垃圾收集器很好地回收吗?

关于内部类和内存泄漏的唯一需要注意的事情是,(非静态)内部类对其封闭对象有隐式引用。这意味着如果您创建了大量内部类的实例,不能指望外部类对象的实例被垃圾收集。

在这种情况下,这意味着如果您泄漏了对history映射的引用,则Example的实例将不能被GC回收。


相关提示:

  • 考虑到您正在使用synchronizedMap,似乎您正在处理一个多线程程序。如果是这种情况,需要注意threshold字段的同步和可见性问题。

  • 如果可能的话,请将threshold字段设置为final。

  • 另一个选择是为您的LinkedHashMap创建一个命名类,并在该类中包含threshold作为一个字段。


6
您无需担心这个依赖,因为每个匿名内部类对象都隐式引用了封闭类的对象。Java 就是这样设计的,而嵌套内部类有这个引用是有原因的,所以从语言规范的角度来看,这是可以编译并且正常的。
关于(缺少)“设计气味”,如果这个匿名类对象完全封装在 Example 类中,在没有其封闭上下文的情况下没有明显的意义,并且在 Example 类之外没有泄漏,那么引用封闭类字段是没有问题的。您只需使用此内部类来分组某些逻辑。
但是,如果此对象泄漏到封闭对象之外(例如,通过 getter 返回它),则应该禁止此操作或将其重构为一个静态内部类,该静态内部类接收 threshold 作为参数。此内部对象持有对封闭对象的引用,可能会阻止GC,从而导致内存泄漏。

1
是的,但反过来不行。内部类对父类有隐式引用,但反过来不成立。 - John
@user3360241 当然可以。我的帖子中没有任何与此相矛盾的内容。 - Forketyfork
1
你的陈述:“你无论如何都有这个依赖关系”暗示了这一点 :) 既然op在询问循环依赖性,那么得出你指的是它是很自然的结论。 - John
1
@user3360241 OP 询问的是 之间的依赖关系,而不是对象。Example 类已经通过实例化依赖于匿名内部类。匿名内部类通过使用其 threshold 字段依赖于封闭类。我只是指出即使没有使用 threshold,匿名内部类也通过引用其实例与封闭类存在依赖关系。 - Forketyfork

2
循环依赖本身并不是坏事,但它可能会导致一些意想不到的内存泄漏。以你的示例为例,目前它很好,因为它可以达到你想要的效果。但是如果你或别人修改了你的代码以暴露你的私有内容:
private Map<String, Progress> history;

那么您可能会遇到麻烦。问题在于,您的内部类会隐式引用Example类,并且无论是否有意,也会将其传递给其他地方。
我现在无法直接引用史蒂夫·麦康奈尔(Steve McConnell)的话,但他在《代码大全》中称循环依赖为反模式。您可以在那里阅读相关内容,或者通过谷歌等搜索引擎深入了解。
除此之外,我能想到的另一个问题是,循环依赖非常难以进行单元测试,因为它们会在对象之间创建非常高的耦合度。
通常情况下,您应该避免循环依赖,除非您有非常好的理由不这样做,例如实现循环链表。

4
“循环依赖关系在单元测试中相当难以处理。”这个人问了一个关于“匿名类”的问题:“如果没有外部类,你怎么能够对匿名类进行单元测试呢?” - mastov

2
任何时候你实例化一个非静态内部类(无论是命名的还是匿名的),这个内部类的实例会自动获得对封闭父类实例的引用。
上面的意思是,如果外部类也持有对非静态内部类的引用(就像在你的代码中一样),那么外部类的实例和非静态内部类的实例之间存在循环依赖关系(无论是命名的还是匿名的)。
在这种情况下,唯一真正的问题是你是否合法地使用了这个现有的交叉引用。在你的具体情况下,我没有看到任何问题 - 非静态内部类使用了封闭外部类的实例变量。对我来说似乎没问题。
在这种情况下,内存泄漏通常发生在将内部类实例的引用传递到外部类之外(这通常是各种监听器的情况) - 因为这个实例有一个对外部类实例的引用,所以外部类无法被垃圾回收。然而,我认为如果你只是交叉引用外部和内部类,不会导致内存泄漏 - 它们将一起被垃圾回收。

1
再说一遍,这是不正确的。父类不必引用内部类。 - John
1
@user3360241,我想我明白你留下评论的原因了。答案已经被编辑过了,现在可以了吗? - Vasiliy

1
我不喜欢你的解决方案(即使我同意这可能有效):
1. 你的Example类应该实现Map或扩展LinkedHashMap,因为阈值实例变量在那里定义,并且使用自己的定义完善了LinkedHashMap的概念。
2. 你的Example类不应该实现Map或扩展LinkedHashMap,因为activate方法没有完善LinkedHashMap或Map,而是使用了Maps的概念。
1 + 2 => 概念问题。

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