JavaRush /Blog Java /Random-ES /Métodos iguales y hashCode: práctica de uso

Métodos iguales y hashCode: práctica de uso

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: al comienzo del curso JavaRush hubo una breve conferencia sobre equals(): ​​léala si la ha olvidado o no la ha visto antes. Los métodos equivalen a &  hashCode: práctica de uso - 1En la lección de hoy hablaremos de estos conceptos en detalle. Créanme, ¡hay mucho de qué hablar! Y antes de pasar a algo nuevo, refresquemos nuestra memoria sobre lo que ya hemos cubierto :) Como recordará, la comparación habitual de dos objetos usando el ==operador “ ” es una mala idea, porque “ ==” compara referencias. Aquí está nuestro ejemplo con autos de una conferencia 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 consola:

false
Parecería que hemos creado dos objetos idénticos de la clase Car: todos los campos de las dos máquinas son iguales, pero el resultado de la comparación sigue siendo falso. Ya sabemos el motivo: los enlaces car1apuntan car2a direcciones diferentes en la memoria, por lo que no son iguales. Todavía queremos comparar dos objetos, no dos referencias. La mejor solución para comparar objetos es equals().

método igual()

Quizás recuerde que no creamos este método desde cero, sino que lo anulamos; después de todo, el método equals()está definido en la clase Object. Sin embargo, 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 clase Object. La misma comparación de enlaces. ¿Por qué fue hecho así? Bueno, ¿cómo saben los creadores del lenguaje qué objetos en su programa se consideran iguales y cuáles no? :) Ésta es la idea principal del método equals(): el propio creador de la clase determina las características mediante las cuales se verifica la igualdad de los objetos de esta clase. Al hacer esto, anulas el método equals()en tu clase. Si no entiendes bien el significado de “tú mismo defines las características”, veamos un ejemplo. Aquí hay una clase simple de persona: 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.
}
Digamos que estamos escribiendo un programa que necesita determinar si dos personas están emparentadas por gemelos o simplemente por dobles. Tenemos cinco características: tamaño de la nariz, color de ojos, peinado, presencia de cicatrices y los resultados de una prueba biológica de ADN (para simplificar, en forma de número de código). ¿Cuál de estas características crees que permitirá a nuestro programa identificar a familiares gemelos? Los métodos equivalen a &  hashCode: práctica de uso - 2Por supuesto, sólo una prueba biológica puede ofrecer una garantía. Dos personas pueden tener el mismo color de ojos, peinado, nariz e incluso cicatrices; hay muchas personas en el mundo y es imposible evitar las coincidencias. Necesitamos un mecanismo fiable: sólo el resultado de una prueba de ADN nos permite sacar una conclusión precisa. ¿Qué significa esto para nuestro método equals()? Necesitamos redefinirlo en una clase Manteniendo en cuenta los requisitos de nuestro programa. El método debe comparar el campo de int dnaCodedos objetos y, si son iguales, entonces los objetos son iguales.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
¿Es realmente así de simple? No precisamente. Nos perdimos algo. En este caso, para nuestros objetos hemos definido sólo un campo "significativo" mediante el cual se establece su igualdad: dnaCode. Ahora imaginemos que no tendríamos 1, sino 50 de esos campos "significativos". Y si los 50 campos de dos objetos son iguales, entonces los objetos son iguales. Esto también podría pasar. El principal problema es que calcular la igualdad de 50 campos es un proceso que requiere mucho tiempo y recursos. Ahora imagina que además de la clase, Mantenemos una clase Womancon exactamente los mismos campos que en Man. Y si otro programador usa tus clases, puede escribir fácilmente en su programa algo como:
public static void main(String[] args) {

   Man man = new Man(........); //un montón de parámetros en el constructor

   Woman woman = new Woman(.........);//mismo grupo de parámetros.

   System.out.println(man.equals(woman));
}
En este caso, no tiene sentido comprobar los valores de los campos: vemos que estamos ante objetos de dos clases diferentes y, en principio, ¡no pueden ser iguales! Esto significa que debemos verificar el método equals(): una comparación de objetos de dos clases idénticas. ¡Qué bueno que hayamos pensado en esto!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
¿Pero tal vez nos olvidamos de algo más? Hmm... ¡Como mínimo, debemos comprobar que no estamos comparando el objeto consigo mismo! Si las referencias A y B apuntan a la misma dirección en la memoria, entonces son el mismo objeto y tampoco 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;
}
Además, no estaría de más añadir una comprobación de null: ningún objeto puede ser igual a null, en cuyo caso no tiene sentido realizar comprobaciones adicionales. Teniendo todo esto en cuenta, nuestro método equals()de clase Manquedará 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. Si resulta que:
  • comparamos dos objetos de la misma clase
  • este no es el mismo objeto
  • No estamos comparando nuestro objeto connull
...luego pasamos a comparar características significativas. En nuestro caso, los campos dnaCodede dos objetos. Al anular un método equals(), asegúrese de cumplir con estos requisitos:
  1. Reflexividad.

    Cualquier objeto debe ser equals()para sí mismo.
    Ya hemos tenido en cuenta este requisito. Nuestro método establece:

    if (this == o) return true;

  2. Simetría.

    Si a.equals(b) == true, entonces b.equals(a)debería regresar true.
    Nuestro método también cumple con 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 la verificación b.equals(c)también debería devolver verdadero.

  4. Permanencia.

    Los resultados del trabajo equals()deberían cambiar solo cuando cambien los campos incluidos en él. Si los datos de dos objetos no han cambiado, los resultados de la verificación equals()siempre deben ser los mismos.

  5. Desigualdad con null.

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

método hashCode()

Ahora hablemos del método hashCode(). ¿Por qué es necesario? Exactamente con el mismo propósito: comparar objetos. ¡ Pero ya lo tenemos equals()! ¿Por qué otro método? La respuesta es simple: mejorar la productividad. Una función hash, que está representada por el método en Java hashCode(), devuelve un valor numérico de longitud fija para cualquier objeto. En el caso de Java, el método hashCode()devuelve un número de 32 bits de tipo int. Comparar dos números entre sí es mucho más rápido que comparar dos objetos usando el método equals(), especialmente si usa muchos campos. Si nuestro programa compara objetos, es mucho más fácil hacerlo mediante código hash, y solo si son iguales hashCode(), procedemos a comparar mediante equals(). Por cierto, así es como funcionan las estructuras de datos basadas en hash (por ejemplo, la que ya conoces HashMap). El método hashCode(), al igual que equals(), lo anula el propio desarrollador. Y al igual que para equals(), el método hashCode()tiene requisitos oficiales especificados en la documentación de Oracle:
  1. Si dos objetos son iguales (es decir, el método equals()devuelve verdadero), deben tener el mismo código hash.

    De lo contrario, nuestros métodos no tendrán sentido. Verificar mediante hashCode(), como dijimos, debería ser lo primero para mejorar el rendimiento. Si los códigos hash son diferentes, la verificación devolverá falso, aunque los objetos sean realmente iguales (como definimos en el método equals()).

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

  3. La regla 1 no funciona a la inversa. 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 sencilla. El método hashCode()regresa int. intes un número de 32 bits. Tiene un número limitado de valores, desde -2.147.483.648 hasta +2.147.483.647. En otras palabras, hay poco más de 4 mil millones de variaciones del número int. Ahora imagina que estás creando un programa para almacenar datos sobre todas las personas vivas en la Tierra. Cada persona tendrá su propio objeto de clase Man. ~7.500 millones de personas viven en la Tierra. En otras palabras, no importa qué tan bueno sea el algoritmo Manque escribamos para convertir objetos en números, simplemente no tendremos suficientes números. Sólo tenemos 4.500 millones de opciones y mucha más gente. Esto significa que no importa cuánto lo intentemos, los códigos hash serán los mismos para diferentes personas. Esta situación (los códigos hash de dos objetos diferentes coinciden) se llama colisión. Uno de los objetivos del programador al anular un método hashCode()es reducir el número potencial de colisiones tanto como sea posible. ¿Cómo será nuestro método hashCode()para la clase Man, teniendo en cuenta todas estas reglas? Como esto:
@Override
public int hashCode() {
   return dnaCode;
}
¿Sorprendido? :) Inesperadamente, pero si miras los requisitos verás que cumplimos con todo. Los objetos para los cuales el nuestro equals()devuelve verdadero serán iguales en hashCode(). Si nuestros dos objetos Mantienen el mismo valor equals(es decir, tienen el mismo valor dnaCode), nuestro método devolverá el mismo número. Veamos un ejemplo más complicado. Digamos que nuestro programa debería seleccionar autos de lujo para clientes coleccionistas. Coleccionar es algo complejo y tiene muchas características. Un coche del año 1963 puede costar 100 veces más que el mismo coche del año 1964. Un coche rojo del año 1970 puede costar 100 veces más que un coche azul de la misma marca del mismo año. Los métodos equivalen a &  hashCode: práctica de uso - 4En el primer caso, con la clase Man, descartamos la mayoría de los campos (es decir, características de la persona) como insignificantes y usamos solo el campo para comparar dnaCode. Aquí estamos trabajando con un área muy singular, ¡y no puede haber detalles menores! Aquí está nuestra clase LuxuryAuto:
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;
   }

   //... captadores, setters, etc.
}
Aquí, a la hora de comparar, debemos tener en cuenta todos los campos. Cualquier error puede costar cientos de miles de dólares al cliente, por lo que es mejor estar 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 método, equals()no nos olvidamos de todos los controles de los que hablamos anteriormente. Pero ahora comparamos cada uno de los tres campos de nuestros objetos. En este programa la igualdad debe ser absoluta, en todos los ámbitos. Qué pasa hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
El campo modelde nuestra clase es una cadena. Esto es conveniente: Stringel método hashCode()ya está anulado en la clase. Calculamos el código hash del campo model, y al mismo le sumamos la suma de los otros dos campos numéricos. Hay un pequeño truco en Java que se utiliza para reducir el número de colisiones: al calcular el código hash, multiplica el resultado intermedio por un número primo impar. El número más comúnmente utilizado es 29 o 31. No entraremos en detalles matemáticos ahora, pero para referencia futura, recuerde que multiplicar resultados intermedios por un número impar lo suficientemente grande ayuda a “dispersar” los resultados del hash. funcionan y terminan con menos objetos con el mismo código hash. Para nuestro método hashCode()en LuxuryAuto se verá 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, hay un punto más importante que vale la pena mencionar. Cada vez que anulamos equals(), hashCode()seleccionamos ciertos campos del objeto, que se tuvieron en cuenta en estos métodos. Pero ¿podemos tener en cuenta diferentes campos en equals()y hashCode()? Técnicamente, podemos. Pero esta 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;
}
Estos son nuestros métodos equals()para hashCode()la clase LuxuryAuto. El método hashCode()permaneció sin cambios y equals()eliminamos el campo del método model. Ahora bien, el modelo no es una característica para comparar dos objetos por equals(). Pero todavía se tiene en cuenta al calcular el código hash. ¿Qué obtendremos como resultado? ¡Creemos dos autos y compruébelo!
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("¿Son estos dos objetos iguales entre sí?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("¿Cuáles son sus códigos hash?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два un objetoа равны друг другу?
true
Какие у них хэш-códigoы?
-1372326051
1668702472
¡Error! ¡Al utilizar campos diferentes para equals()y hashCode()violamos el contrato establecido para ellos! Dos objetos iguales equals()deben tener el mismo código hash. Tenemos diferentes significados para ellos. Estos errores pueden tener las consecuencias más increíbles, especialmente cuando se trabaja con colecciones que utilizan hashes. Por lo tanto, al redefinir equals()será hashCode()correcto utilizar los mismos campos. La conferencia resultó bastante larga, ¡pero hoy aprendiste muchas cosas nuevas! :) ¡Es hora de volver a resolver problemas!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION