redefinir - ¿Qué problemas se deben considerar cuando se reemplaza equals y hashCode en Java?
redefinir equals en java (11)
La teoría (para los abogados del lenguaje y la inclinación matemática):
equals()
( javadoc ) debe definir una relación de equivalencia (debe ser reflexiva , simétrica y transitiva ). Además, debe ser coherente (si los objetos no se modifican, debe seguir devolviendo el mismo valor). Además, o.equals(null)
siempre debe devolver false.
hashCode()
( javadoc ) también debe ser consistente (si el objeto no se modifica en términos de equals()
, debe seguir devolviendo el mismo valor).
La relación entre los dos métodos es:
Siempre que
a.equals(b)
, entoncesa.hashCode()
debe ser igual queb.hashCode()
.
En la práctica:
Si anula uno, debe anular el otro.
Use el mismo conjunto de campos que usa para calcular equals()
para calcular hashCode()
.
Utilice las excelentes clases de ayuda 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();
}
}
También recuerda:
Al usar una Collection o Map basado en hash como HashSet , LinkedHashSet , HashMap , Hashtable o WeakHashMap , asegúrese de que el código hashCode () de los objetos clave que coloque en la colección nunca cambie mientras el objeto esté en la colección. La forma a prueba de balas para asegurar esto es hacer que sus llaves sean inmutables, lo que también tiene otros beneficios .
¿Qué problemas / dificultades se deben considerar cuando se reemplaza a equals
y hashCode
?
¡Hay algunos problemas que vale la pena notar si está tratando con clases que persisten usando un Mapeador de relaciones de objetos (ORM) como Hibernate, si no pensó que esto ya era irrazonablemente complicado!
Los objetos cargados perezosos son subclases
Si sus objetos persisten utilizando un ORM, en muchos casos tratará con proxies dinámicos para evitar cargar objetos desde el almacén de datos antes de tiempo. Estos proxies se implementan como subclases de su propia clase. Esto significa que this.getClass() == o.getClass()
devolverá 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 está tratando con un ORM, usar o instanceof Person
es lo único que se comportará correctamente.
Los objetos cargados perezosos tienen campos nulos
Los ORM suelen utilizar los captadores para forzar la carga de objetos cargados perezosos. Esto significa que person.name
será null
si person
está cargado de forma perezosa, incluso si person.getName()
fuerza la carga y devuelve "John Doe". En mi experiencia, esto surge más a menudo en hashCode()
y equals()
.
Si está tratando con un ORM, asegúrese de usar siempre getters, y nunca las referencias de campo en hashCode()
y equals()
.
Guardar un objeto cambiará su estado
Los objetos persistentes a menudo usan un campo de id
para mantener la clave del objeto. Este campo se actualizará automáticamente cuando se guarde un objeto por primera vez. No uses 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 puede incluir getId()
en hashCode()
. Si lo hace, cuando un objeto persiste, su hashCode
cambia. Si el objeto está en un HashSet
, "nunca" lo encontrarás de nuevo.
En mi ejemplo de Person
, probablemente usaría getName()
para hashCode
y getId()
más getName()
(solo para paranoia) para equals()
. Está bien si hay algún riesgo de "colisiones" para hashCode()
, pero nunca está bien para equals()
.
hashCode()
debe usar el subconjunto no cambiante de propiedades de equals()
El método equals () se utiliza para determinar la igualdad de dos objetos.
como el valor int de 10 es siempre igual a 10. Pero este método equals () trata sobre la igualdad de dos objetos. Cuando decimos objeto, tendrá propiedades. Para decidir sobre la igualdad se consideran esas propiedades. No es necesario que todas las propiedades se tengan en cuenta para determinar la igualdad y con respecto a la definición de la clase y el contexto, se puede decidir. Entonces el método equals () puede ser anulado.
Siempre debemos anular el método hashCode () siempre que anulemos el método equals (). Si no, ¿qué pasará? Si utilizamos tablas hash en nuestra aplicación, no se comportará como se espera. Como el hashCode se usa para determinar la igualdad de valores almacenados, no devolverá el valor correspondiente correcto para una clave.
La implementación predeterminada dada es el método hashCode () en la clase de Objeto utiliza la dirección interna del objeto y lo convierte en entero y lo devuelve.
public class Tiger {
private String color;
private String stripePattern;
private int height;
@Override
public boolean equals(Object object) {
boolean result = false;
if (object == null || object.getClass() != getClass()) {
result = false;
} else {
Tiger tiger = (Tiger) object;
if (this.color == tiger.getColor()
&& this.stripePattern == tiger.getStripePattern()) {
result = true;
}
}
return result;
}
// just omitted null checks
@Override
public int hashCode() {
int hash = 3;
hash = 7 * hash + this.color.hashCode();
hash = 7 * hash + this.stripePattern.hashCode();
return hash;
}
public static void main(String args[]) {
Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
Tiger siberianTiger = new Tiger("White", "Sparse", 4);
System.out.println("bengalTiger1 and bengalTiger2: "
+ bengalTiger1.equals(bengalTiger2));
System.out.println("bengalTiger1 and siberianTiger: "
+ bengalTiger1.equals(siberianTiger));
System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
System.out.println("siberianTiger hashCode: "
+ siberianTiger.hashCode());
}
public String getColor() {
return color;
}
public String getStripePattern() {
return stripePattern;
}
public Tiger(String color, String stripePattern, int height) {
this.color = color;
this.stripePattern = stripePattern;
this.height = height;
}
}
Código de salida de ejemplo:
bengalTiger1 and bengalTiger2: true
bengalTiger1 and siberianTiger: false
bengalTiger1 hashCode: 1398212510
bengalTiger2 hashCode: 1398212510
siberianTiger hashCode: –1227465966
Hay dos métodos en super clase como java.lang.Object. Necesitamos anularlos a objetos personalizados.
public boolean equals(Object obj)
public int hashCode()
Los objetos iguales deben producir el mismo código hash siempre que sean iguales, sin embargo, los objetos desiguales no necesitan producir códigos hash distintos.
public class Test
{
private int num;
private String data;
public boolean equals(Object obj)
{
if(this == obj)
return true;
if((obj == null) || (obj.getClass() != this.getClass()))
return false;
// object must be Test at this point
Test test = (Test)obj;
return num == test.num &&
(data == test.data || (data != null && data.equals(test.data)));
}
public int hashCode()
{
int hash = 7;
hash = 31 * hash + num;
hash = 31 * hash + (null == data ? 0 : data.hashCode());
return hash;
}
// other methods
}
Si desea obtener más, consulte este enlace como http://www.javaranch.com/journal/2002/10/equalhash.html
Este es otro ejemplo, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html
¡Que te diviertas! @. @
Hay un par de formas de verificar su igualdad de clases antes de verificar la igualdad de miembros, y creo que ambas son útiles en las circunstancias correctas.
- Utilice el operador
instanceof
. - Use
this.getClass().equals(that.getClass())
.
Utilizo el # 1 en una implementación final
igual a igual, o cuando implemento una interfaz que prescribe un algoritmo para iguales (como las interfaces de colección java.util
, la forma correcta de verificar con (obj instanceof Set)
o la interfaz que esté implementando) . Por lo general, es una mala elección cuando los iguales se pueden anular porque eso rompe la propiedad de simetría.
La opción # 2 permite que la clase se extienda de manera segura sin anular iguales o romper la simetría.
Si su clase también es Comparable
, los métodos equals
y compareTo
deben ser consistentes. Aquí hay una plantilla para el método equals en una clase Comparable
:
final class MyClass implements Comparable<MyClass>
{
…
@Override
public boolean equals(Object obj)
{
/* If compareTo and equals aren''t final, we should check with getClass instead. */
if (!(obj instanceof MyClass))
return false;
return compareTo((MyClass) obj) == 0;
}
}
Lógicamente tenemos:
a.getClass().equals(b.getClass()) && a.equals(b)
⇒ a.hashCode() == b.hashCode()
¡Pero no al revés!
Para iguales, mira en Secretos de iguales por Angelika Langer . Yo la amo mucho. Ella también es un gran FAQ sobre Genéricos en Java . Vea sus otros artículos here (desplácese hacia abajo hasta "Core Java"), donde también continúa con la Parte 2 y la "comparación de tipos mixtos". ¡Diviértete leyéndolos!
Para una implementación amigable con la herencia, consulte la solución de Tal Cohen. ¿Cómo implemento correctamente el método equals ()?
Resumen:
En su libro Effective Java Programming Language Guide (Addison-Wesley, 2001), Joshua Bloch afirma que "Simplemente no hay forma de extender una clase de instanciable y agregar un aspecto al tiempo que se preserva el contrato de iguales". Tal no está de acuerdo.
Su solución es implementar equals () llamando a otro ciego no simétricoEquals () en ambos sentidos. blindlyEquals () se anula por las subclases, es igual a () se hereda, y nunca se anula.
Ejemplo:
class Point {
private int x;
private int y;
protected boolean blindlyEquals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return (p.x == this.x && p.y == this.y);
}
public boolean equals(Object o) {
return (this.blindlyEquals(o) && o.blindlyEquals(this));
}
}
class ColorPoint extends Point {
private Color c;
protected boolean blindlyEquals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint)o;
return (super.blindlyEquals(cp) &&
cp.color == this.color);
}
}
Tenga en cuenta que equals () debe funcionar en las jerarquías de herencia si se debe cumplir con el principio de sustitución de Liskov .
Todavía me sorprende que ninguno haya recomendado la biblioteca de guayaba para esto.
//Sample taken from a current working project of mine just to illustrate the idea
@Override
public int hashCode(){
return Objects.hashCode(this.getDate(), this.datePattern);
}
@Override
public boolean equals(Object obj){
if ( ! obj instanceof DateAndPattern ) {
return false;
}
return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
&& Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
}
Una aclaración sobre el obj.getClass() != getClass()
.
Esta declaración es el resultado de que equals()
es una herencia hostil. La JLS (especificación del lenguaje Java) especifica que si A.equals(B) == true
entonces B.equals(A)
también debe devolver true
. Si omite esa instrucción que hereda las clases que anulan equals()
(y cambia su comportamiento), se romperá esta especificación.
Considere el siguiente ejemplo de lo que sucede cuando se omite la declaración:
class A {
int field1;
A(int field1) {
this.field1 = field1;
}
public boolean equals(Object other) {
return (other != null && other instanceof A && ((A) other).field1 == field1);
}
}
class B extends A {
int field2;
B(int field1, int field2) {
super(field1);
this.field2 = field2;
}
public boolean equals(Object other) {
return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
}
}
Hacer new A(1).equals(new A(1))
Además, los new B(1,1).equals(new B(1,1))
dan como resultado un resultado verdadero, como debería ser.
Todo se ve muy bien, pero mira qué sucede si intentamos usar ambas clases:
A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;
Obviamente, esto está mal.
Si quiere asegurarse la condición simétrica. a = b si b = a y el principio de sustitución de Liskov llama a super.equals(other)
no solo en el caso de la instancia de B
, sino que verifica después la instancia de A
:
if (other instanceof B )
return (other != null && ((B)other).field2 == field2 && super.equals(other));
if (other instanceof A) return super.equals(other);
else return false;
Que saldrá:
a.equals(b) == true;
b.equals(a) == true;
Donde, si a
no es una referencia de B
, entonces podría ser una referencia de clase A
(porque la extiendes), en este caso también llamas super.equals()
.
Una cosa que he encontrado es que dos objetos contienen referencias entre sí (un ejemplo es una relación padre / hijo con un método de conveniencia en el padre para obtener todos los hijos).
Este tipo de cosas son bastante comunes cuando se hacen asignaciones de Hibernate, por ejemplo.
Si incluye ambos extremos de la relación en su código de hash o pruebas iguales, es posible entrar en un ciclo recursivo que termina en una excepción Exception.
La solución más sencilla es no incluir la colección getChildren en los métodos.