【问题标题】:Circular dependency in Java constructorsJava构造函数中的循环依赖
【发布时间】:2011-04-08 10:46:24
【问题描述】:

我有以下课程。

public class B 
{
    public A a;

    public B()
    {
        a= new A();
        System.out.println("Creating B");
    }
}

public class A 
{
    public B b;

    public A()
    {
        b = new B();
        System.out.println("Creating A");
    }

    public static void main(String[] args) 
    {
        A a = new A();
    }
}

可以清楚地看到,类之间存在循环依赖关系。如果我尝试运行 A 类,我最终会得到一个StackOverflowError

如果创建了一个依赖关系图,其中节点是类,那么这种依赖关系可以很容易地识别(至少对于具有少量节点的图)。那么为什么 JVM 至少在运行时没有识别出来呢? JVM 至少可以在开始执行之前发出警告,而不是抛出 StackOverflowError

[更新] 一些语言不能有循环依赖,因为那样的话源代码将无法构建。例如,see this question 和接受的答案。如果循环依赖是 C# 的一种设计味道,那么为什么它不是 Java 的呢?只是因为Java可以(编译循环依赖的代码)?

[update2] 最近发现jCarder。根据该网站,它通过动态检测 Java 字节码并在对象图中查找循环来发现潜在的死锁。谁能解释一下该工具是如何找到循环的?

【问题讨论】:

  • 您为什么会收到警告?您是否在某处读到 JVM 会为您执行此操作?
  • 这类问题对于开发人员来说很容易检测到,并且首先。 JVM 倾向于警告您无法轻易检测到的问题,例如损坏的类文件。
  • 我喜欢 5 个答案中只有 2 个(截至我写这篇文章的时候)真正回答你的问题:why doesn't the compiler detect and warn about the potential issue。这两个都不是投票最高的(同样,至少在我写这篇文章的时候)。
  • @BertF:七年后,仍然如此。
  • 那么谁选择了接受的答案?

标签: java constructor circular-dependency


【解决方案1】:

A 类的构造函数调用 B 类的构造函数。B 类的构造函数调用 A 类的构造函数。你有一个无限递归调用,这就是为什么你最终得到一个 StackOverflowError

Java支持类之间的循环依赖,这里的问题只与构造函数相互调用有关。

您可以尝试以下方法:

A a = new A();
B b = new B();

a.setB(b);
b.setA(a);

【讨论】:

  • 但是循环依赖不是很糟糕吗?如果您实际上在代码中具有循环依赖关系(例如您给出的示例),这难道不是设计不良的标志吗?如果是,那么为什么java支持它?如果不是,那你能指出一些涉及循环依赖的设计是首选的情况吗?
  • 生产者/消费者或任何回调/事件情况如何?这样的事情最终会发生,尽管可能并不完全取决于两个班级。也许是接口。
  • 在这种情况下,“依赖”的含义还不是很清楚。依赖关系通常转化为X 需要在Y 之前发生
  • 举一个现实生活中的例子:例如,您可以将A 重命名为Parent 并将B 重命名为Child。这种关系在计算中经常出现(Parent 类需要知道它的孩子是怎样的,Child 类需要知道Parent 是谁),例如处理 XML 文档时(子标签包含在父标签等中)
  • 这没有回答问题(关于为什么适当的工具在运行前没有检测到问题)。
【解决方案2】:

在 Java 中,在 2 个类之间建立循环关系是完全有效的(尽管可能会询问有关设计的问题),但是在您的情况下,您有每个实例在其构造函数中创建另一个实例的异常行为(这是 StackOverflowError 的实际原因)。

这种特定模式称为相互递归,其中您有 2 个方法 A 和 B(构造函数大多只是方法的一种特殊情况),A 调用 B,B 调用 A。检测它们之间的关系中的无限循环在琐碎的情况下(您提供的那个)有两种方法是可能的,但是为一般情况解决它类似于解决停止问题。鉴于解决停机问题是不可能的,即使是简单的情况,编译器通常也不会费心去尝试。

使用FindBugs 模式可能涵盖一些简单的情况,但并非对所有情况都正确。

【讨论】:

    【解决方案3】:

    不一定像您的示例中那样简单。我相信解决这个问题就等于解决halting problem,众所周知,这是不可能的。

    【讨论】:

    • 我同意,确定复杂案例是否存在循环依赖可能不可行。但是,我们可以近似,在这种情况下,JVM 可能只是表明存在潜在的循环依赖。然后开发人员可以查看代码。
    • 我认为大多数情况下编译器可以通过近似检测在合理的时间内是那些直接跳到你身上的情况(就像你问题中的例子)。将此作为要求将使编写编译器变得非常困难,而收益却很少。
    • JVM 正在通过堆栈限制逼近此测试。具有无限堆栈的更强大的执行模型将无法停止;堆栈帧限制是准确找到此类问题的理想机制。我认为你几乎永远不会遇到不是循环依赖的堆栈溢出。
    【解决方案4】:

    如果你真的有这样的用例,你可以按需(懒惰地)创建对象并使用 getter:

    public class B 
    {
        private A a;
    
        public B()
        {
            System.out.println("Creating B");
        }
    
        public A getA()
        {
          if (a == null)
            a = new A();
    
          return a;
        }
    }
    

    (对于 A 类也是如此)。所以只有必要的对象才会被创建,如果你例如做:

    a.getB().getA().getB().getA()
    

    【讨论】:

    • 不是最佳实践,而是一个绝妙的想法!
    【解决方案5】:

    使用组合和构造函数注入依赖项的 getter/setter 类似的解决方法。需要注意的重要一点是对象不会为其他类创建实例,它们是传入(也称为注入)。

    public interface A {}
    public interface B {}
    
    public class AProxy implements A {
        private A delegate;
    
        public void setDelegate(A a) {
            delegate = a;
        }
    
        // Any implementation methods delegate to 'delegate'
        // public void doStuff() { delegate.doStuff() }
    }
    
    public class AImpl implements A {
        private final B b;
    
        AImpl(B b) {
            this.b = b;
        }
    }
    
    public class BImpl implements B {
        private final A a;
    
        BImpl(A a) {
            this.a = a;
        }
    }
    
    public static void main(String[] args) {
        A proxy = new AProxy();
        B b = new BImpl(proxy);
        A a = new AImpl(b);
        proxy.setDelegate(a);
    }
    

    【讨论】:

    • 我不明白这是如何解决问题的,因为当注入 A 时,您依赖 B 的实现者在 B 的构造函数中不调用 A 上的任何方法。这样做会导致在代理上发生方法调用,这最终会导致 NullPointerException,因为您希望代理 A 上的那些调用(这将是 null)。
    • 对于 getter/setter 情况也可以这样说。这里的要点是,要创建实例,必须将参数传递给构造函数。这里的代理实例允许打破循环依赖的“后期绑定”。干净吗?不,这是一个可行的解决方案吗?是的,在短期内,但绝对不是在长期内。正确的解决方案将是一个更好的设计,其中 A 和 B 相互忽略。例如:C c = new C(A​​, B)
    猜你喜欢
    • 1970-01-01
    • 2015-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多