2010-09-05 21 views
25

Ho le seguenti classi.Dipendenza circolare nelle classi java

public class B 
{ 
    public A a; 

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

e

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

Come si vede chiaramente, v'è una dipendenza circolare tra le classi. se provo a eseguire la classe A, alla fine ottengo uno StackOverflowError.

Se viene creato un grafico di dipendenza, in cui i nodi sono classi, questa dipendenza può essere facilmente identificata (almeno per i grafici con pochi nodi). Allora perché la JVM non lo identifica, almeno in fase di runtime? Invece di lanciare StackOverflowError, JVM può almeno dare un avvertimento prima di iniziare l'esecuzione.

[Aggiornamento] Alcune lingue non possono avere dipendenze circolari, perché quindi il codice sorgente non verrà generato. Ad esempio, see this question e la risposta accettata. Se la dipendenza circolare è un odore di progettazione per C# allora perché non è per Java? Solo perché Java può (compilare codice con dipendenze circolari)?

[update2] Trovato di recente jCarder. Secondo il sito web, trova potenziali deadlock, strumentando dinamicamente i codici byte Java e cercando i cicli nel grafico degli oggetti. Qualcuno può spiegare in che modo lo strumento trova i cicli?

+0

Perché si aspetta di ottenere un avvertimento su questo? Hai letto da qualche parte che JVM farà questo per te? – Cratylus

+1

Il tipo di problema è molto semplice per lo sviluppatore da rilevare e prima. La JVM tende ad avvertire problemi di attacco che non è possibile rilevare facilmente, come un file di classe corrotto. –

+0

Mi piace come solo 2 delle 5 risposte (al momento in cui scrivo questo) rispondono veramente alla tua domanda: "perché il compilatore non rileva e avvisa del potenziale problema?". E nessuno di questi due è il più votato (di nuovo, almeno nel momento in cui scrivo questo). –

risposta

22

Il costruttore della classe A chiama il costruttore della classe B. Il costruttore della classe B chiama il costruttore della classe A. Hai una chiamata di ricorsione infinita, ecco perché hai un StackOverflowError.

Supporti Java con dipendenze circolari tra classi, il problema qui è solo relativo ai costruttori che si chiamano l'un l'altro.

Si può provare con qualcosa di simile:

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

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

Ma le dipendenze circolari non sono buone? Se si dispone effettivamente di dipendenze circolari nel codice (come nell'esempio fornito), non è un indicatore di progettazione errata? Se sì, allora perché java lo supporta? In caso contrario, puoi indicarmi alcuni casi in cui è preferibile il design che coinvolge la dipendenza circolare? – athena

+0

Come su produttore/consumatore o qualsiasi situazione di callback/evento? Qualcosa di simile finirà per accadere, anche se probabilmente non dipende letteralmente tra due classi. Forse interfacce. –

+2

Non è molto chiaro cosa significhi "dipendenza" in questo caso. La dipendenza spesso si traduce in 'X' necessario per * accadere * prima di' Y'. –

10

Non è necessariamente facile come nel tuo esempio. Credo che risolvere questo problema equivarrebbe a risolvere il halting problem che - come tutti sappiamo - è impossibile.

+0

Sono d'accordo, determinare se esiste una dipendenza circolare per casi complessi potrebbe non essere fattibile. Ma possiamo approssimare, nel qual caso la JVM potrebbe solo indicare che esiste una potenziale dipendenza circolare. Lo sviluppatore può quindi rivedere il codice. – athena

+0

Penso che la maggior parte dei casi che un compilatore potrebbe rilevare per approssimazione __ in un tempo ragionevole__ siano quelli che ti saltano addosso (come nell'esempio nella tua domanda). Avere questo come requisito renderebbe molto difficile la scrittura dei compilatori mentre guadagna poco. – musiKk

+1

La JVM sta approssimando questo test, tramite il limite dello stack. Un modello di esecuzione più robusto con uno stack infinito semplicemente non si fermerebbe; le limitazioni dello stack frame sono un meccanismo desiderabile per trovare esattamente questo tipo di problema. Direi che non incontrerai mai uno stack overflow che non è una dipendenza circolare. –

11

Il suo perfettamente valido in Java per avere un rapporto circolare tra 2 classi (anche se le domande si potrebbe chiedere circa il disegno), ma nel tuo caso si ha l'insolito azione di ogni istanza che crea un'istanza dell'altro nel suo costruttore (questa è la causa effettiva dello StackOverflowError).

Questo modello particolare è noto un ricorsione mutua dove si hanno 2 metodi A e B (un costruttore è più solo un caso particolare di un metodo) e A chiama B e B chiama A. Rilevare un loop infinito nel rapporto tra questi due metodi sono possibili nel caso banale (quello che hai fornito), ma risolverlo per il generale è come risolvere il problema dell'arresto. Dato che risolvere il problema dell'arresto è impossibile, in genere i compilatori non si preoccupano nemmeno di provare i casi semplici.

Potrebbe essere possibile coprire alcuni dei casi semplici utilizzando uno schema FindBugs, ma non sarebbe corretto per tutti i casi.

0

Una soluzione simile ai getter/setter che utilizzano la composizione e l'iniezione del costruttore per le dipendenze. La cosa più importante da notare è che gli oggetti non creano l'istanza per le altre classi, ma vengono passati (ovvero l'iniezione).

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

Non vedo come questo risolva il problema, dal momento che si sta facendo affidamento sull'implementatore di B per non chiamare alcun metodo su A nel costruttore di B quando A è iniettata. Ciò farebbe sì che il richiamo del metodo si verifichi sul proxy, il che alla fine si tradurrebbe in una NullPointerException dal momento che si desidera eseguire il proxy di tali chiamate su A (che sarebbe nullo). – marchaos

+0

Lo stesso si può dire per il caso getter/setter. Il punto qui è che per creare un'istanza, un parametro deve essere passato al costruttore. L'istanza proxy qui consente un "binding tardivo" che interrompe la dipendenza circolare. È pulito? No. È una soluzione praticabile? Sì, a breve termine ma sicuramente non a lungo termine. La soluzione corretta sarebbe un design migliore dove A e B sono ignari l'uno dell'altro. es .: C c = new C (A, B) – gpampara

3

Se avete davvero un caso d'uso come questo, è possibile creare gli oggetti su richiesta (pigramente) e utilizzare 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; 
    } 
} 

(e allo stesso modo per la classe A). Quindi vengono creati solo gli oggetti necessari se ad es. fanno:

a.getB().getA().getB().getA() 
+0

Non è la migliore pratica ma una bella idea! – ozma