web-dev-qa-db-fra.com

Dépendance circulaire dans les classes Java

J'ai les cours suivants.

public class B 
{
    public A a;

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

et

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

Comme on peut le voir clairement, il existe une dépendance circulaire entre les classes. si j'essaye d'exécuter la classe A, j'obtiens finalement un StackOverflowError.

Si un graphe de dépendances est créé, où les nœuds sont des classes, alors cette dépendance peut être facilement identifiée (au moins pour les graphes avec peu de nœuds). Alors pourquoi la JVM ne l'identifie-t-elle pas, du moins au moment de l'exécution? Au lieu de lancer StackOverflowError, la JVM peut au moins donner un avertissement avant de démarrer l'exécution.

[Mise à jour] Certaines langues ne peuvent pas avoir de dépendances circulaires, car alors le code source ne se construira pas. Par exemple, voir cette question et la réponse acceptée. Si la dépendance circulaire est une odeur de conception pour C #, pourquoi ne l'est-elle pas pour Java? Uniquement parce que Java can (compiler du code avec des dépendances circulaires)?

[update2] Récemment trouvé jCarder . Selon le site Web, il trouve des blocages potentiels en instrumentant dynamiquement des codes Java octets et en recherchant des cycles dans le graphique d'objet. Quelqu'un peut-il expliquer comment l'outil trouve les cycles?

33
athena

Le constructeur de votre classe A appelle le constructeur de la classe B. Le constructeur de la classe B appelle le constructeur de la classe A. Vous avez un appel de récursion infini, c'est pourquoi vous finissez par avoir un StackOverflowError.

Java prend en charge les dépendances circulaires entre les classes, le problème ici n'est lié qu'aux constructeurs qui s'appellent les uns les autres.

Vous pouvez essayer avec quelque chose comme:

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

a.setB(b);
b.setA(a);
32
Vivien Barousse

Son parfaitement valide dans Java pour avoir une relation circulaire entre 2 classes (bien que des questions puissent être posées sur la conception), cependant dans votre cas, vous avez l'action inhabituelle de chaque instance créant une instance de la autre dans son constructeur (c'est la cause réelle de StackOverflowError).

Ce modèle particulier est connu comme une récursion mutuelle où vous avez 2 méthodes A et B (un constructeur est principalement un cas particulier d'une méthode) et A appelle B et B appelle A. Détecter une boucle infinie dans la relation entre ces 2 méthodes est possible dans le cas trivial (celui que vous avez fourni), mais le résoudre pour le général revient à résoudre le problème d'arrêt. Étant donné que la résolution du problème d'arrêt est impossible, les complices ne prennent généralement pas la peine d'essayer, même pour les cas simples.

Il pourrait être possible de couvrir quelques cas simples en utilisant un modèle FindBugs , mais ce ne serait pas correct pour tous les cas.

14
Michael Barker

Ce n'est pas nécessairement aussi facile que dans votre exemple. Je crois que résoudre ce problème équivaudrait à résoudre le problème d'arrêt qui - comme nous le savons tous - est impossible.

11
musiKk

Si vous avez vraiment un cas d'utilisation comme celui-ci, vous pouvez créer les objets à la demande (paresseusement) et utiliser un 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;
    }
}

(et de même pour la classe A). Ainsi, seuls les objets nécessaires sont créés si, par exemple, faire:

a.getB().getA().getB().getA()
3
Andre Holzner

Une solution de contournement similaire aux getters/setters utilisant la composition et l'injection de constructeur pour les dépendances. La grande chose à noter est que les objets ne créent pas l'instance aux autres classes, ils sont passés (alias injection).

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);
}
0
gpampara