Круговая зависимость в конструкторах Java

У меня следующие занятия.

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 не идентифицирует это, по крайней мере, во время выполнения? Вместо того, чтобы бросать StackOverflowError, JVM может, по крайней мере, выдать предупреждение перед запуском выполнения.

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

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


person athena    schedule 05.09.2010    source источник
comment
Почему вы ожидаете получить предупреждение об этом? Вы где-то читали, что JVM сделает это за вас?   -  person Cratylus    schedule 05.09.2010
comment
Разработчику очень легко обнаружить такую ​​проблему в первую очередь. JVM имеет тенденцию предупреждать о проблемах, которые вы не можете легко обнаружить, например о поврежденном файле класса.   -  person Peter Lawrey    schedule 05.09.2010
comment
Мне нравится, как только 2 из 5 ответов (на момент написания этого) действительно отвечают на ваш вопрос: why doesn't the compiler detect and warn about the potential issue. И ни один из этих двух не получил наибольшего количества голосов (опять же, по крайней мере, в то время, когда я это пишу).   -  person Bert F    schedule 05.09.2010
comment
@BertF: семь лет спустя, все еще верно.   -  person Olivier Cailloux    schedule 27.03.2018
comment
Кто тогда выбрал принятый ответ?   -  person Nery Ortez    schedule 14.07.2018


Ответы (5)


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

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

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

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

a.setB(b);
b.setA(a);
person Vivien Barousse    schedule 05.09.2010
comment
Но разве круговые зависимости не плохи? Если у вас действительно есть циклические зависимости в коде (как в приведенном вами примере), разве это не показатель плохого дизайна? Если да, то почему Java поддерживает это? Если нет, то не могли бы вы указать мне на некоторые случаи, когда предпочтительнее использовать дизайн с циклической зависимостью? - person athena; 05.09.2010
comment
Как насчет производителя / потребителя или любой ситуации обратного вызова / события? Что-то подобное в конечном итоге произойдет, хотя, вероятно, не в буквальном смысле между двумя классами. Может интерфейсы. - person Domingo Ignacio; 05.09.2010
comment
Не совсем понятно, что в данном случае означает «зависимость». Зависимость часто переводится как X должно произойти до Y. - person Andre Holzner; 05.09.2010
comment
Приведем реальный пример: вы можете, например, переименовать A в Parent и B в Child. Такие отношения часто возникают в вычислениях (класс Parent должен знать, каков его дочерний элемент, а класс Child должен знать, кто такой Parent), например при работе с XML-документами (дочерние теги заключаются в родительские теги и т. д.) - person Andre Holzner; 05.09.2010
comment
Это не отвечает на вопрос (почему проблема не обнаруживается адекватными инструментами до выполнения). - person Olivier Cailloux; 27.03.2018
comment
@OlivierCailloux, хотя в данном случае это кажется очевидным, обнаружение переполнения стека в целом неразрешимо. - person setholopolus; 05.12.2019
comment
Чтобы привести больше примеров из реальной жизни ... String зависит от Integer (String.valueOf(int i) использует Integer.toString(i);); и Integer зависит от String (у него есть несколько toString методов). Иногда это неизбежно в коде, хотя да, избегать этого там, где это целесообразно или там, где это может вызвать проблемы - хорошая идея. - person BeUndead; 10.02.2020

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

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

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

person Michael Barker    schedule 05.09.2010

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

person musiKk    schedule 05.09.2010
comment
Я согласен, определение наличия круговой зависимости для сложных случаев может оказаться невозможным. Но мы можем приблизиться, и в этом случае JVM может просто указать, что существует потенциальная циклическая зависимость. Затем разработчик может просмотреть код. - person athena; 05.09.2010
comment
Я думаю, что в большинстве случаев компилятор может обнаружить приближением в разумные сроки именно те, которые прыгают прямо на вас (как в примере в вашем вопросе). Наличие этого в качестве требования сделало бы написание компиляторов очень трудным, но при этом мало что выиграло бы. - person musiKk; 05.09.2010
comment
JVM приближается к этому тесту через ограничение стека. Более надежная модель выполнения с бесконечным стеком просто не остановится; Ограничения кадра стека - желательный механизм для поиска именно такого рода проблем. Я бы сказал, что вы почти никогда не столкнетесь с переполнением стека, которое не является циклической зависимостью. - person Mark McKenna; 05.10.2011

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

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()
person Andre Holzner    schedule 05.09.2010
comment
Не лучшая практика, но прекрасная идея! - person ozma; 27.07.2017

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

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);
}
person gpampara    schedule 05.09.2010
comment
Я не понимаю, как это решает проблему, поскольку вы полагаетесь на то, что разработчик B не будет вызывать какие-либо методы для A в конструкторе B, когда A вводится. Это приведет к вызову метода на прокси-сервере, что в конечном итоге приведет к исключению NullPointerException, так как вы хотите, чтобы эти вызовы прокси выполнялись на A (который был бы нулевым). - person marchaos; 11.05.2012
comment
То же самое можно сказать и о случае геттера / сеттера. Дело в том, что для создания экземпляра в конструктор должен быть передан параметр. Экземпляр прокси здесь допускает позднюю привязку, которая нарушает циклическую зависимость. Это чисто? Нет. Это жизнеспособное решение? Да, в краткосрочной перспективе, но определенно не в долгосрочной перспективе. Правильным решением будет лучший дизайн, в котором A и B не обращают внимания друг на друга. например: C c = новый C (A, B) - person gpampara; 14.05.2012