¿Qué cuestiones se deben considerar al anular iguales y hashCode en Java?
¿Qué problemas/obstáculos se deben considerar al anular equals
y hashCode
?
La teoría (para los lingüistas y los aficionados a las matemáticas):
equals()
( javadoc ) debe definir una relación de equivalencia (debe ser reflexiva , simétrica y transitiva ). Además, debe ser consistente (si los objetos no se modifican, entonces debe seguir devolviendo el mismo valor). Además, o.equals(null)
siempre debe devolver falso.
hashCode()
( javadoc ) también debe ser consistente (si el objeto no se modifica en términos deequals()
, debe seguir devolviendo el mismo valor).
La relación entre los dos métodos es:
Siempre
a.equals(b)
, entoncesa.hashCode()
debe ser igual queb.hashCode()
.
En la práctica:
Si anula uno, entonces debería anular el otro.
Utilice el mismo conjunto de campos que utiliza para calcular equals()
para calcularhashCode()
.
Utilice las excelentes clases auxiliares EqualsBuilder y HashCodeBuilder de la biblioteca Apache Commons Lang . Un ejemplo:
public class Person {
private String name;
private int age;
// ...
@Override
public int hashCode() {
return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
// if deriving: appendSuper(super.hashCode()).
append(name).
append(age).
toHashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person))
return false;
if (obj == this)
return true;
Person rhs = (Person) obj;
return new EqualsBuilder().
// if deriving: appendSuper(super.equals(obj)).
append(name, rhs.name).
append(age, rhs.age).
isEquals();
}
}
Recuerda también:
Cuando utilice una colección o un mapa basado en hash, como HashSet , LinkedHashSet , HashMap , Hashtable o WeakHashMap , asegúrese de que el hashCode() de los objetos clave que coloque en la colección nunca cambie mientras el objeto esté en la colección. La forma infalible de garantizar esto es hacer que las claves sean inmutables, lo que también tiene otros beneficios .
Hay algunos problemas que vale la pena notar si estás tratando con clases que persisten usando un Mapeador de relaciones de objetos (ORM) como Hibernate, ¡si no pensabas que esto ya era irrazonablemente complicado!
Los objetos cargados de forma diferida son subclases
Si sus objetos persisten mediante un ORM, en muchos casos tendrá que tratar con servidores proxy dinámicos para evitar cargar objetos demasiado pronto desde el almacén de datos. Estos servidores proxy se implementan como subclases de su propia clase. Esto significa que this.getClass() == o.getClass()
volverá false
. Por ejemplo:
Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy
Si se trata de un ORM, el uso o instanceof Person
es lo único que se comportará correctamente.
Los objetos cargados de forma diferida tienen campos nulos
Los ORM suelen utilizar captadores para forzar la carga de objetos cargados de forma diferida. Esto significa que person.name
será null
si person
se carga de forma diferida, incluso si person.getName()
fuerza la carga y devuelve "John Doe". En mi experiencia, esto surge con más frecuencia en hashCode()
y equals()
.
Si está tratando con un ORM, asegúrese de utilizar siempre captadores y nunca incluir referencias en hashCode()
y equals()
.
Guardar un objeto cambiará su estado
Los objetos persistentes suelen utilizar un id
campo para contener la clave del objeto. Este campo se actualizará automáticamente cuando se guarde un objeto por primera vez. No utilice un campo de identificación en hashCode()
. Pero puedes usarlo en equals()
.
Un patrón que uso a menudo es
if (this.getId() == null) {
return this == other;
}
else {
return this.getId().equals(other.getId());
}
Pero: no puedes incluirlo getId()
en hashCode()
. Si lo hace, cuando un objeto persiste, cambia hashCode
. Si el objeto está en un HashSet
, "nunca" lo volverás a encontrar.
En mi Person
ejemplo, probablemente usaría getName()
for hashCode
y getId()
plus getName()
(solo por paranoia) for equals()
. Está bien si existe algún riesgo de "colisiones" para hashCode()
, pero nunca está bien para equals()
.
hashCode()
debe utilizar el subconjunto de propiedades que no cambian deequals()