CodeGym /Blog Java /Random-ES /Métodos equals y hashCode: mejores prácticas
Autor
Milan Vucic
Programming Tutor at Codementor.io

Métodos equals y hashCode: mejores prácticas

Publicado en el grupo Random-ES
¡Hola! Hoy hablaremos de dos métodos importantes en Java: equals()y hashCode(). Esta no es la primera vez que los conocemos: el curso de CodeGym comienza con una breve lección sobre equals(): ​​léala si la olvidó o no la ha visto antes... Métodos equals y hashCode: mejores prácticas - 1En la lección de hoy, hablaremos sobre estos conceptos en detalle. Y créeme, ¡tenemos algo de qué hablar! Pero antes de pasar a lo nuevo, actualicemos lo que ya hemos cubierto :) Como recordará, generalmente es una mala idea comparar dos objetos usando el ==operador, porque ==compara referencias. Aquí está nuestro ejemplo con autos de una lección reciente:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Salida de la consola:

false
Parece que hemos creado dos Carobjetos idénticos: los valores de los campos correspondientes de los dos objetos coche son los mismos, pero el resultado de la comparación sigue siendo falso. Ya sabemos el motivo: las referencias car1y car2apuntan a direcciones de memoria diferentes, por lo que no son iguales. Pero aún queremos comparar los dos objetos, no dos referencias. La mejor solución para comparar objetos es el equals()método.

método igual()

Puede recordar que no creamos este método desde cero, sino que lo anulamos: el equals()método se define en la Objectclase. Dicho esto, en su forma habitual, es de poca utilidad:

public boolean equals(Object obj) {
   return (this == obj);
}
Así es como equals()se define el método en la Objectclase. Esta es una comparación de referencias una vez más. ¿Por qué lo hicieron así? Bueno, ¿cómo saben los creadores del lenguaje qué objetos en su programa se consideran iguales y cuáles no? :) Este es el punto principal del equals()método: el creador de una clase es quien determina qué características se utilizan al verificar la igualdad de los objetos de la clase. Luego anula el equals()método en su clase. Si no comprende del todo el significado de "determina qué características", consideremos un ejemplo. Aquí hay una clase simple que representa a un hombre: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Supongamos que estamos escribiendo un programa que necesita determinar si dos personas son gemelas idénticas o simplemente se parecen. Tenemos cinco características: el tamaño de la nariz, el color de los ojos, el peinado, la presencia de cicatrices y los resultados de las pruebas de ADN (para simplificar, lo representamos como un código entero). ¿Cuál de estas características cree que permitiría a nuestro programa identificar gemelos idénticos? Métodos equals y hashCode: mejores prácticas - 2Por supuesto, solo una prueba de ADN puede proporcionar una garantía. Dos personas pueden tener el mismo color de ojos, corte de pelo, nariz e incluso cicatrices: hay muchas personas en el mundo y es imposible garantizar que no haya doppelgängers por ahí. Pero necesitamos un mecanismo confiable: solo el resultado de una prueba de ADN nos permitirá llegar a una conclusión precisa. ¿Qué significa esto para nuestro equals()método? Tenemos que anularlo en elManclase, teniendo en cuenta los requisitos de nuestro programa. El método debe comparar el int dnaCodecampo de los dos objetos. Si son iguales, entonces los objetos son iguales.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
¿Es realmente tan simple? No precisamente. Pasamos por alto algo. Para nuestros objetos, identificamos solo un campo que es relevante para establecer la igualdad de objetos: dnaCode. Ahora imagina que no tenemos 1, sino 50 campos relevantes. Y si los 50 campos de dos objetos son iguales, entonces los objetos son iguales. Tal escenario también es posible. El principal problema es que establecer la igualdad comparando 50 campos es un proceso que requiere mucho tiempo y recursos. Ahora imagine que además de nuestra Manclase, tenemos una Womanclase con exactamente los mismos campos que existen en Man. Si otro programador usa nuestras clases, podría fácilmente escribir un código como este:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
En este caso, verificar los valores del campo no tiene sentido: podemos ver fácilmente que tenemos objetos de dos clases diferentes, ¡así que no hay forma de que puedan ser iguales! Esto significa que debemos agregar una verificación al equals()método, comparando las clases de los objetos comparados. ¡Es bueno que hayamos pensado en eso!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
¿Pero tal vez nos hemos olvidado de algo más? Hmm... ¡Como mínimo, deberíamos comprobar que no estamos comparando un objeto consigo mismo! Si las referencias A y B apuntan a la misma dirección de memoria, entonces son el mismo objeto y no necesitamos perder tiempo comparando 50 campos.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tampoco está de más agregar una marca para null: ningún objeto puede ser igual a null. Entonces, si el parámetro del método es nulo, entonces no tiene sentido realizar verificaciones adicionales. Con todo esto en mente, nuestro equals()método para la Manclase se ve así:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Realizamos todas las comprobaciones iniciales mencionadas anteriormente. Al final del día, si:
  • estamos comparando dos objetos de la misma clase
  • y los objetos comparados no son el mismo objeto
  • y el objeto pasado no esnull
...luego procedemos a una comparación de las características relevantes. Para nosotros, esto significa los dnaCodecampos de los dos objetos. Al anular el equals()método, asegúrese de observar estos requisitos:
  1. reflexividad.

    Cuando el equals()método se utiliza para comparar cualquier objeto consigo mismo, debe devolver verdadero.
    Ya hemos cumplido con este requisito. Nuestro método incluye:

    
    if (this == o) return true;
    

  2. Simetría.

    Si a.equals(b) == true, entonces b.equals(a)debe regresar true.
    Nuestro método también satisface este requisito.

  3. Transitividad.

    Si dos objetos son iguales a un tercer objeto, entonces deben ser iguales entre sí.
    Si a.equals(b) == truey a.equals(c) == true, entonces b.equals(c)también debe devolver verdadero.

  4. Persistencia.

    El resultado de equals()debe cambiar solo cuando se modifican los campos involucrados. Si los datos de los dos objetos no cambian, entonces el resultado de equals()debe ser siempre el mismo.

  5. Desigualdad con null.

    Para cualquier objeto, a.equals(null)debe devolver falso
    Esto no es solo un conjunto de algunas "recomendaciones útiles", sino un contrato estricto , establecido en la documentación de Oracle

método hashCode()

Ahora hablemos del hashCode()método. ¿Por qué es necesario? Exactamente con el mismo propósito: comparar objetos. ¡Pero ya tenemos equals()! ¿Por qué otro método? La respuesta es simple: para mejorar el rendimiento. Una función hash, representada en Java usando el hashCode()método, devuelve un valor numérico de longitud fija para cualquier objeto. En Java, el hashCode()método devuelve un número de 32 bits ( int) para cualquier objeto. Comparar dos números es mucho más rápido que comparar dos objetos usando el equals()método, especialmente si ese método considera muchos campos. Si nuestro programa compara objetos, esto es mucho más sencillo de hacer usando un código hash. Solo si los objetos son iguales según el hashCode()método, la comparación continúa hasta el final.equals()método. Por cierto, así es como funcionan las estructuras de datos basadas en hash, por ejemplo, el conocido HashMap! El desarrollador anula el hashCode()método, al igual que el método. equals()Y al igual que equals(), el hashCode()método tiene requisitos oficiales detallados en la documentación de Oracle:
  1. Si dos objetos son iguales (es decir, el equals()método devuelve verdadero), entonces deben tener el mismo código hash.

    De lo contrario, nuestros métodos no tendrían sentido. Como mencionamos anteriormente, hashCode()primero se debe realizar una verificación para mejorar el rendimiento. Si los códigos hash fueran diferentes, la verificación devolvería falso, aunque los objetos sean realmente iguales según cómo hayamos definido el equals()método.

  2. Si el hashCode()método se llama varias veces en el mismo objeto, debe devolver el mismo número cada vez.

  3. La regla 1 no funciona en la dirección opuesta. Dos objetos diferentes pueden tener el mismo código hash.

La tercera regla es un poco confusa. ¿Cómo puede ser esto? La explicación es bastante simple. El hashCode()método devuelve un int. An intes un número de 32 bits. Tiene un rango de valores limitado: desde -2.147.483.648 hasta +2.147.483.647. En otras palabras, hay poco más de 4 mil millones de valores posibles para un int. Ahora imagine que está creando un programa para almacenar datos sobre todas las personas que viven en la Tierra. Cada persona corresponderá a su propio Personobjeto (similar a la Manclase). Hay ~7.500 millones de personas viviendo en el planeta. En otras palabras, no importa cuán inteligente sea el algoritmo que escribimos para convertirPersonobjetos a un int, simplemente no tenemos suficientes números posibles. Solo tenemos 4.500 millones de valores int posibles, pero hay mucha más gente que eso. Esto significa que no importa cuánto lo intentemos, algunas personas diferentes tendrán los mismos códigos hash. Cuando esto sucede (los códigos hash coinciden para dos objetos diferentes), lo llamamos colisión. Al anular el hashCode()método, uno de los objetivos del programador es minimizar el número potencial de colisiones. Teniendo en cuenta todas estas reglas, ¿cómo se hashCode()verá el método en la Personclase? Como esto:

@Override
public int hashCode() {
   return dnaCode;
}
¿Sorprendido? :) Si te fijas en los requisitos, verás que los cumplimos todos. Los objetos para los que nuestro equals()método devuelve verdadero también serán iguales según hashCode(). Si nuestros dos Personobjetos son iguales equals(es decir, tienen el mismo dnaCode), entonces nuestro método devuelve el mismo número. Consideremos un ejemplo más difícil. Suponga que nuestro programa debe seleccionar autos de lujo para coleccionistas de autos. Coleccionar puede ser una afición compleja y con muchas peculiaridades. Un automóvil particular de 1963 puede costar 100 veces más que un automóvil de 1964. Un auto rojo de 1970 puede costar 100 veces más que un auto azul de la misma marca del mismo año. Métodos equals y hashCode: mejores prácticas - 4En nuestro ejemplo anterior, con la Personclase, descartamos la mayoría de los campos (es decir, características humanas) como insignificantes y usamos solo eldnaCodecampo en las comparaciones. ¡Ahora estamos trabajando en un ámbito muy idiosincrásico, en el que no hay detalles insignificantes! Aquí está nuestra LuxuryAutoclase:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Ahora debemos considerar todos los campos en nuestras comparaciones. Cualquier error podría costarle a un cliente cientos de miles de dólares, por lo que sería mejor ser demasiado seguro:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
En nuestro equals()método, no hemos olvidado todos los controles de los que hablamos anteriormente. Pero ahora comparamos cada uno de los tres campos de nuestros objetos. Para este programa, necesitamos la igualdad absoluta, es decir, la igualdad de cada campo. ¿Sobre qué hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
El modelcampo de nuestra clase es un String. Esto es conveniente, porque la Stringclase ya anula el hashCode()método. Calculamos el modelcódigo hash del campo y luego le agregamos la suma de los otros dos campos numéricos. Los desarrolladores de Java tienen un truco simple que usan para reducir la cantidad de colisiones: al calcular un código hash, multiplique el resultado intermedio por un número primo impar. El número más utilizado es 29 o 31. No profundizaremos en las sutilezas matemáticas ahora, pero en el futuro recuerda que multiplicar los resultados intermedios por un número impar lo suficientemente grande ayuda a "esparcir" los resultados de la función hash y, en consecuencia, reduzca la cantidad de objetos con el mismo código hash. Para nuestro hashCode()método en LuxuryAuto, se vería así:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Puede leer más sobre todas las complejidades de este mecanismo en esta publicación en StackOverflow , así como en el libro Java efectivo de Joshua Bloch. Finalmente, otro punto importante que vale la pena mencionar. Cada vez que reemplazamos el método equals()y hashCode(), seleccionamos ciertos campos de instancia que se tienen en cuenta en estos métodos. Estos métodos consideran los mismos campos. Pero, ¿podemos considerar diferentes campos en equals()y hashCode()? Técnicamente, podemos. Pero esto es una mala idea, y he aquí por qué:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Aquí están nuestros métodos equals()y hashCode()para la LuxuryAutoclase. El hashCode()método permaneció sin cambios, pero eliminamos el modelcampo del equals()método. El modelo ya no es una característica utilizada cuando el equals()método compara dos objetos. Pero al calcular el código hash, ese campo aún se tiene en cuenta. ¿Qué obtenemos como resultado? ¡Creemos dos autos y descubramos!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
¡Error! Al usar diferentes campos para los métodos equals()y hashCode(), ¡violamos los contratos que se han establecido para ellos! Dos objetos que son iguales según el equals()método deben tener el mismo código hash. Recibimos diferentes valores para ellos. Dichos errores pueden tener consecuencias absolutamente increíbles, especialmente cuando se trabaja con colecciones que usan un hash. Como resultado, cuando anule equals()y hashCode(), debe considerar los mismos campos. Esta lección fue bastante larga, ¡pero aprendiste mucho hoy! :) ¡Ahora es el momento de volver a resolver tareas!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION