Круговая зависимость в Java-классах - PullRequest
32 голосов
/ 05 сентября 2010

У меня есть следующие классы.

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();
    }
}

Как ясно видно, существует круговая зависимость между классами.если я попытаюсь запустить класс А, я в итоге получу StackOverflowError.

Если создается граф зависимостей, где узлами являются классы, то эту зависимость можно легко идентифицировать (по крайней мере, для графов с небольшим количеством узлов).Тогда почему JVM не идентифицирует это, по крайней мере, во время выполнения?Вместо броска StackOverflowError JVM может по крайней мере дать предупреждение перед началом выполнения.

[Обновить] Некоторые языки не могут иметь циклические зависимости, потому что тогда исходный код не будет собираться.Например, см. Этот вопрос и принятый ответ.Если циклическая зависимость является запахом дизайна для C #, то почему она не для Java?Только потому, что Java может (компилировать код с круговыми зависимостями)?

[update2] Недавно найден jCarder .Согласно веб-сайту, он обнаруживает потенциальные тупики, динамически инструментируя байтовые коды Java и просматривая циклы в графе объектов.Кто-нибудь может объяснить, как инструмент находит циклы?

Ответы [ 5 ]

31 голосов
/ 05 сентября 2010

Конструктор вашего класса A вызывает конструктор класса B. Конструктор класса B вызывает конструктор класса A. У вас бесконечный рекурсивный вызов, поэтому в итоге вы получите StackOverflowError.

Java поддерживает циклические зависимости между классами, проблема здесь связана только с конструкторами, вызывающими друг друга.

Вы можете попробовать что-то вроде:

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

a.setB(b);
b.setA(a);
14 голосов
/ 05 сентября 2010

Вполне допустимо в Java иметь круговые отношения между 2 классами (хотя можно задавать вопросы о дизайне), однако в вашем случае вы выполняете необычное действие каждого экземпляра, создавая экземпляр другого в своем конструкторе (это являющаяся фактической причиной StackOverflowError).

Этот конкретный шаблон известен как взаимная рекурсия, когда у вас есть 2 метода A и B (конструктор - это в основном просто особый случай метода), а A вызывает B, а B вызывает A. Обнаружение бесконечного цикла в отношениях между этими В тривиальном случае возможен 2 метода (тот, который вы предоставили), но решение его для общего сродни решению проблемы остановки. Учитывая, что решение проблемы остановки невозможно, компиляторы, как правило, не пытаются даже для простых случаев.

Может быть возможно охватить несколько простых случаев с использованием шаблона FindBugs , но это не будет правильным для всех случаев.

11 голосов
/ 05 сентября 2010

Это не обязательно так просто, как в вашем примере. Я считаю, что решение этой проблемы было бы равносильно решению проблемы остановки , что, как мы все знаем, невозможно.

3 голосов
/ 05 сентября 2010

Если у вас действительно есть такой вариант использования, вы можете создавать объекты по требованию (лениво) и использовать метод получения:

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()
0 голосов
/ 05 сентября 2010

Подобный обходной путь для получателей / установщиков, использующих состав и инжектор конструктора для зависимостей. Важно отметить, что объекты не создают экземпляр другим классам, они передаются (иначе как инъекция).

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);
}
...