2010-09-05 38 views
25

Tengo las siguientes clases.Dependencia circular en clases Java

public class B 
{ 
    public A a; 

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

y

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

Como puede verse claramente, hay una dependencia circular entre las clases. si trato de ejecutar la clase A, eventualmente obtengo un StackOverflowError.

Si se crea un gráfico de dependencia, donde los nodos son clases, entonces esta dependencia se puede identificar fácilmente (al menos para gráficos con pocos nodos). Entonces, ¿por qué la JVM no identifica esto, al menos en tiempo de ejecución? En lugar de arrojar StackOverflowError, JVM puede al menos dar una advertencia antes de comenzar la ejecución.

[Actualización] Algunos idiomas no pueden tener dependencias circulares, porque entonces el código fuente no se compilará. Por ejemplo, see this question y la respuesta aceptada. Si la dependencia circular es un olor de diseño para C#, entonces ¿por qué no es para Java? Solo porque Java puede (compilar código con dependencias circulares)?

[actualización2] Recientemente encontrado jCarder. Según el sitio web, encuentra bloqueos potenciales instrumentando dinámicamente los códigos de bytes de Java y buscando ciclos en el gráfico de objetos. ¿Alguien puede explicar cómo encuentra la herramienta los ciclos?

+0

¿Por qué se puede esperar para obtener una advertencia sobre esto? ¿Leyó en alguna parte que JVM hará esto por usted? – Cratylus

+1

El tipo de problema es muy fácil de detectar para el desarrollador y primero. La JVM tiende a advertir problemas que no se pueden detectar fácilmente, como un archivo de clase corrupto. –

+0

Me encanta cómo solo 2 de las 5 respuestas (a la hora de escribir esto) realmente responden a su pregunta: '¿por qué el compilador no detecta y advierte sobre el posible problema? Y ninguno de esos 2 es el más votado (nuevamente, al menos en el momento en que escribo esto). –

Respuesta

22

El constructor de su clase A llama al constructor de clase B. El constructor de clase B llama al constructor de clase A. Tiene una llamada de recursión infinita, por eso termina teniendo un StackOverflowError.

Java admite tener dependencias circulares entre clases, el problema aquí solo está relacionado con los constructores que se llaman entre sí.

Usted puede tratar con algo como:

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

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

¿Pero no son malas las dependencias circulares? Si realmente tiene dependencias circulares en el código (como el ejemplo que dio), ¿no es un indicador de mal diseño? Si es así, ¿por qué lo soporta Java? Si no, ¿me puede indicar algunos casos en los que se prefiere el diseño que implica dependencia circular? – athena

+0

¿Qué tal productor/consumidor o cualquier situación de devolución de llamada/evento? Algo así terminará sucediendo, aunque probablemente no dependa literalmente entre dos clases. Quizás interfaces. –

+2

No está muy claro qué significa 'dependencia' en este caso. La dependencia a menudo se traduce en 'X' necesita' suceder * antes de 'Y'. –

10

No es necesariamente tan fácil como en su ejemplo. Creo que resolver este problema equivaldría a resolver el halting problem que, como todos sabemos, es imposible.

+0

Acepto, determinar si existe una dependencia circular para casos complejos puede no ser factible. Pero, podemos aproximarnos, en cuyo caso la JVM podría indicar que existe una posible dependencia circular. El desarrollador puede revisar el código. – athena

+0

Creo que la mayoría de los casos que un compilador puede detectar por aproximación __en un tiempo razonable__ son aquellos que te saltan directamente (como el ejemplo en tu pregunta). Tener esto como un requisito haría que escribir los compiladores fuera muy difícil mientras ganaba poco. – musiKk

+1

La JVM está aproximándose a esta prueba, a través del límite de la pila. Un modelo de ejecución más robusto con una pila infinita simplemente no podría detenerse; las limitaciones del marco de pila son un mecanismo deseable para encontrar exactamente este tipo de problema. Yo diría que casi nunca encontrará un desbordamiento de pila que no sea una dependencia circular. –

11

Su perfectamente válido en Java para tener una relación circular entre 2 clases (aunque las preguntas podrían ser preguntado por el diseño), sin embargo, en su caso, usted tiene la inusual acción de cada instancia creando una instancia de la otra en su constructor (siendo esta la causa real de StackOverflowError).

Este patrón particular se conoce como recursión mutua en la que tiene 2 métodos A y B (un constructor es solo un caso especial de un método) y A llama a las llamadas B y B. Detecta un ciclo infinito en la relación entre estos 2 métodos son posibles en el caso trivial (el que usted ha suministrado), pero resolverlo para el general es similar a la solución del problema de detención. Dado que la solución del problema de detención es imposible, los cumplidores en general no se molestan en intentar incluso para los casos simples.

Es posible cubrir algunos casos simples usando un patrón FindBugs, pero no sería correcto para todos los casos.

0

Una solución similar a los getters/setters que usan la composición y la inyección de constructor para las dependencias. Lo más importante a tener en cuenta es que los objetos no crean la instancia para las otras clases, sino que se pasan (también conocido como inyección).

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

No veo cómo esto resuelve el problema, ya que confía en que el implementador de B no invoque ningún método en A en el constructor de B cuando A es inyectado. Hacerlo causaría que la invocación del método ocurriera en el proxy, lo que en última instancia resultaría en una NullPointerException ya que le gustaría utilizar esas llamadas en el A (que sería nulo). – marchaos

+0

Lo mismo se puede decir del caso getter/setter. El punto aquí es que para crear una instancia, un parámetro debe pasarse al constructor. La instancia de proxy aquí permite una "vinculación tardía" que rompe la dependencia circular. ¿Está limpio? No. ¿Es una solución viable? Sí, a corto plazo, pero definitivamente no a largo plazo. La solución correcta sería un mejor diseño donde A y B son ajenos el uno al otro. por ejemplo: C c = nuevo C (A, B) – gpampara

3

Si realmente tiene un caso de uso de este tipo, se podría crear los objetos a demanda (pereza) y utilizar un captador:

public class B 
{ 
    private A a; 

    public B() 
    { 
     System.out.println("Creating B"); 
    } 

    public A getA() 
    { 
     if (a == null) 
     a = new A(); 

     return a; 
    } 
} 

(y lo mismo para la clase A). Por lo tanto, solo se crean los objetos necesarios si, por ejemplo, Qué:

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

¡No es la mejor práctica sino una bella idea! – ozma