2012-01-14 20 views
31

Tengo dos clases definidas de manera que ambas contienen referencias al otro objeto. Ellos similar a esto (esto es simplificada, en mi dominio real de la clase del modelo A contiene una lista de B y cada B tiene una referencia de nuevo a los padres A):Implementando equals y hashCode para objetos con referencias circulares en Java

public class A { 

    public B b; 
    public String bKey; 

    @Override 
    public int hashCode() { 
     final int prime = 31; 
     int result = 1; 
     result = prime * result + ((b == null) ? 0 : b.hashCode()); 
     result = prime * result + ((bKey == null) ? 0 : bKey.hashCode()); 
     return result; 
    } 
    @Override 
    public boolean equals(Object obj) { 
     if (this == obj) 
      return true; 
     if (obj == null) 
      return false; 
     if (!(obj instanceof A)) 
      return false; 
     A other = (A) obj; 
     if (b == null) { 
      if (other.b != null) 
       return false; 
     } else if (!b.equals(other.b)) 
      return false; 
     if (bKey == null) { 
      if (other.bKey != null) 
       return false; 
     } else if (!bKey.equals(other.bKey)) 
      return false; 
     return true; 
    } 
} 

public class B { 

    public A a; 
    public String aKey; 

    @Override 
    public int hashCode() { 
     final int prime = 31; 
     int result = 1; 
     result = prime * result + ((a == null) ? 0 : a.hashCode()); 
     result = prime * result + ((aKey == null) ? 0 : aKey.hashCode()); 
     return result; 
    } 
    @Override 
    public boolean equals(Object obj) { 
     if (this == obj) 
      return true; 
     if (obj == null) 
      return false; 
     if (!(obj instanceof B)) 
      return false; 
     B other = (B) obj; 
     if (a == null) { 
      if (other.a != null) 
       return false; 
     } else if (!a.equals(other.a)) 
      return false; 
     if (aKey == null) { 
      if (other.aKey != null) 
       return false; 
     } else if (!aKey.equals(other.aKey)) 
      return false; 
     return true; 
    } 
} 

El hashCode y equals se han generado por Eclipse usando ambos campos de A y B. El problema es que llamar al método equals o hashCode en cualquier objeto da como resultado un StackOverflowError ya que ambos llaman al otro objeto equals y hashCode método. Por ejemplo, el siguiente programa fallará con StackOverflowError usando los objetos anteriores:

public static void main(String[] args) { 

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

     A a1 = new A(); 
     B b1 = new B(); 
     a1.b = b1; 
     b1.a = a1; 

     System.out.println(a.equals(a1)); 
    } 

Si hay algo intrínsecamente malo en tener un modelo de dominio definido con las relaciones circulares de esta manera, por favor hágamelo saber. Por lo que puedo decir, aunque este es un escenario bastante común, ¿correcto?

¿Cuál es la mejor práctica para definir hashCode y equals en este caso? Quiero mantener todos los campos en el método equals para que sea una verdadera comparación de igualdad profunda en el objeto, pero no veo cómo puedo hacerlo con este problema. ¡Gracias!

+4

¿Por qué necesitan los niños a tener referencias a los padres? Esto va a complicarle la vida con respecto a la serialización – I82Much

+2

Estoy trabajando con un modelo de dominio heredado. Mi serialización deja esencialmente fuera la relación de los padres en el niño y luego corrige la relación en la deserialización. ¿Tener referencias circulares no es algo común en los modelos de dominio? La relación de base de datos subyacente es OneToMany que se diseñó mediante ingeniería inversa mediante herramientas JBoss en objetos JPA CON las referencias circulares. – Tom

Respuesta

5

Estoy de acuerdo con el comentario de I82Much que debe evitar tener B haciendo referencia a sus padres: es la duplicación de información, que generalmente solo causa problemas, pero es posible que deba hacerlo en su caso.

aunque deje la referencia de los padres en B, en lo que se refiere a códigos hash debe ignorar por completo la referencia de los padres y sólo utilizar los verdaderos las variables internas del B para construir el código hash.

Los A s son solo contenedores y su valor está totalmente determinado por su contenido, que es el valor de B s, y también sus claves hash.

Si A es un conjunto desordenado, hay que tener mucho cuidado de que el código hash se está construyendo a partir de los valores (o BB códigos hash) no depende de algún orden. Por ejemplo, si el código hash se construye sumando y multiplicando los códigos hash de los B contenidos en alguna secuencia, primero debe ordenar los códigos hash aumentando el orden antes de calcular el resultado de las sumas/multiplicaciones. Del mismo modo, A.equals(o) no debe depender del orden de B s (si no está configurado).

Tenga en cuenta que si está utilizando un java.util.Collection dentro A, a continuación, sólo la fijación de la B s código hash haciendo caso omiso de la referencia de los padres dará automáticamente A códigos hash válidos desde los Collection s tienen buenas códigos hash por defecto (ordenamiento o no) .

+0

No veo exactamente cómo es esta duplicación de información como usted dice. Uno no puede tratar estrictamente con A, por lo que puede inferir que el padre de B es A porque está referenciado desde A. Usted solo puede manejar B y necesita saber algo sobre la A de la que se hace referencia, ¿correcto? Esa información no existiría si B no tuviera una referencia de regreso a A. – Tom

+0

Depende de lo que esté haciendo; tal vez realmente necesites encontrar el padre de B. Pero generalmente debe diseñar su código de forma tal que pueda evitarlo. Si no puede evitarlo, debe tener mucho cuidado de no entrar en un estado incoherente. – toto2

+0

Diría que una lista doblemente vinculada tiene duplicación de datos y es útil para la eficiencia en ciertas situaciones. Sin embargo, nunca puede entrar en un estado incoherente porque la API es tal que ambos lados de la propiedad siempre se modifican simultáneamente. – toto2

3

En un modelo típico, la mayoría de las entidades tienen una ID única. Esta ID es útil en varios casos de uso (en particular: recuperación/búsqueda de la base de datos). IIUC, se supone que el campo bKey es una ID única. Por lo tanto, la práctica común para comparar dichas entidades es comparar su identificación:

@Override 
public boolean equals(Object obj) { 
    if (obj == null) 
     return false; 
    if (!getClass().equals(obj.getClass())) 
     return false; 
    return this.bKey.equals(((B) obj).bKey); 
} 


@Override 
public int hashCode() { return bKey.hashCode(); } 

Usted puede preguntar: "¿Qué pasa si dos B objetos tienen el mismo ID, pero estado diferente (valor de sus campos son diferentes)". Su código debe asegurarse de que tales cosas no sucedan. Esto será un problema independientemente de cómo implemente equals() o hashCode() porque básicamente significa que tiene dos versiones diferentes de la misma entidad en su sistema y no podrá decir cuál es la correcta.

+0

Esto tiene sentido para mí. En mi caso, sin embargo, quería un método de igual a igual para mis pruebas de junit serializador personalizado para hacer uso de. En mis pruebas unitarias, quería asegurarme de que el objeto se serializara y deserializara de la manera correcta, por lo que compruebo cada campo. En la práctica, mi modelo de dominio probablemente estará bien al hacer que mi método igual pruebe el ID del objeto. – Tom

+0

Esto ayuda a abordar mi pregunta sobre mejores prácticas. – Tom

-2

En primer lugar, ¿está seguro de que desea anular Equals() y GetHashCode()? En la mayoría de los escenarios, debería estar bien con la igualdad referencial predeterminada.

Pero, supongamos que no. Entonces, ¿cuál es la semántica de igualdad apropiada que quieres?

Por ejemplo digamos que cada A tiene un campo de tipo BgetB y cada B tiene un campo de tipo AgetA. Deje a1 y a2 ser dos objetos A, tener los mismos campos y el mismo getB (igual que en "la misma dirección de memoria") b1. ¿Son a1 y a2 iguales? Supongamos que b1.getA es lo mismo que a1 (lo mismo que en "misma dirección de memoria") pero no es lo mismo que a2. ¿Todavía quiere considerar a1 y a2 iguales?

De lo contrario, no anule nada y use la igualdad referencial predeterminada.

Si es así, entonces aquí hay una solución: Deje que A tenga una función int GetCoreHashCode() que no dependa del elemento getB, (pero dependa de otros campos). Deje que B tenga una función int GetCoreHashCode() que no dependa del elemento getA, (pero dependa de otros campos). Ahora deje que la función int GetHashCode() de A dependa de this.GetCoreHashCode() y getB.GetCoreHashCode() y del mismo modo para B, y listo.

+0

Esto probablemente funcione, sin embargo, estoy buscando las mejores prácticas del modelo de dominio frente a una solución específica para el problema recursivo demostrado. – Tom

0

Puede tener dos sabores de equals - la anulación de Object.equals y la que es más adecuada para la recursión. La comprobación recursiva de igualdad toma una A o una B, cualquiera que sea la otra clase de esta, que es el objeto al que llama igualdad recursiva en nombre de. Si lo llama en nombre del this.equals, pase null. Por ejemplo:

A { 
    ... 
    @Override 
    public boolean equals(Object obj) { 
     // check for this, null, instanceof... 
     A other = (A) obj; 
     return recursiveEquality(other, null); 
    } 

    // package-private, optionally-recursive equality 
    boolean recursiveEquality(A other, B onBehalfOf) { 
     if (onBehalfOf != null) { 
      assert b != onBehalfOf; 
      // we got here from within a B.equals(..) call, so we just need 
      // to check that our B is the same as the one that called us. 
     } 
     // At this point, we got called from A.equals(Object). So, 
     // need to recurse. 
     else if (b == null) { 
      if (other.b != null) 
       return false; 
     } 
     // B has a similar structure. Call its recursive-aware equality, 
     // passing in this for the onBehalfOf 
     else if (!b.recursiveEquality(other.b, this)) 
      return false; 

     // check bkey and return 
    } 
} 

Así que, siguiendo A.equals:

  1. A.equals llamadas `recursiveEquality (Othera, null)
    1. si this.b != null, terminamos en el tercer bloque if-else, que llama b.recursiveEquality(other.b, this)
      1. en B.recursiveEquality, llegamos a la primera i bloque f-else, que simplemente afirma que nuestro A es el mismo que se nos pasó (es decir, que la referencia circular no está rota)
      2. finalizamos B.recursiveEquality marcando aKey (dependiendo de sus invariantes, es posible que desee afirmar algo basado en lo que sucedió en el paso 3).B.recursiveEquality vuelve
    2. terminamos A.recursiveEquality comprobando bKey, posiblemente con similares afirma
  2. A.equals devuelve el resultado de la verificación de la igualdad recursiva
+0

Esto probablemente funcione, sin embargo, estoy buscando las mejores prácticas del modelo de dominio frente a una solución específica para el problema recursivo demostrado. – Tom

Cuestiones relacionadas