JavaRush /Blog Java /Random-ES /Contratos iguales y hashCode o lo que sea
Aleksandr Zimin
Nivel 1
Санкт-Петербург

Contratos iguales y hashCode o lo que sea

Publicado en el grupo Random-ES
La gran mayoría de los programadores de Java, por supuesto, saben que los métodos equalsestán hashCodeestrechamente relacionados entre sí y que es aconsejable anular ambos métodos en sus clases de forma coherente. Un número un poco menor sabe por qué esto es así y qué tristes consecuencias pueden ocurrir si se infringe esta regla. Propongo considerar el concepto de estos métodos, repetir su propósito y comprender por qué están tan conectados. Escribí este artículo, como el anterior sobre la carga de clases, para mí mismo, para finalmente revelar todos los detalles del problema y no volver a fuentes de terceros. Por lo tanto, estaré encantado de recibir críticas constructivas, porque si hay lagunas en alguna parte, deberían eliminarse. El artículo, lamentablemente, resultó bastante extenso.

es igual a anular reglas

En Java se requiere un método equals()para confirmar o negar el hecho de que dos objetos del mismo origen son lógicamente iguales . Es decir, al comparar dos objetos, el programador necesita comprender si sus campos significativos son equivalentes . No es necesario que todos los campos sean idénticos, ya que el método equals()implica igualdad lógica . Pero a veces no es necesario utilizar este método. Como suele decirse, la forma más sencilla de evitar problemas al utilizar un determinado mecanismo es no utilizarlo. También cabe señalar que una vez que rompes un contrato, equalspierdes el control de comprender cómo otros objetos y estructuras interactuarán con tu objeto. Y posteriormente será muy difícil encontrar la causa del error.

Cuándo no anular este método

  • Cuando cada instancia de una clase es única.
  • En mayor medida, esto se aplica a aquellas clases que proporcionan un comportamiento específico en lugar de estar diseñadas para trabajar con datos. Como, por ejemplo, la clase Thread. Para ellos equals, la implementación del método proporcionado por la clase Objectes más que suficiente. Otro ejemplo son las clases de enumeración ( Enum).
  • Cuando en realidad la clase no está obligada a determinar la equivalencia de sus instancias.
  • Por ejemplo, para una clase java.util.Randomno hay necesidad alguna de comparar instancias de la clase entre sí, para determinar si pueden devolver la misma secuencia de números aleatorios. Simplemente porque la naturaleza de esta clase ni siquiera implica tal comportamiento.
  • Cuando la clase que estás extendiendo ya tiene su propia implementación del método equalsy el comportamiento de esta implementación te conviene.
  • Por ejemplo, para las clases , la Setimplementación está en y respectivamente. ListMapequalsAbstractSetAbstractListAbstractMap
  • Y finalmente, no es necesario anular equalscuándo el alcance de su clase es privateo package-privatey está seguro de que este método nunca será llamado.

contrato igual

Al anular un método, equalsel desarrollador debe cumplir con las reglas básicas definidas en la especificación del lenguaje Java.
  • Reflexividad
  • para cualquier valor dado x, la expresión x.equals(x)debe devolver true.
    Dado - es decir, tal quex != null
  • Simetría
  • para cualquier valor dado xy y, x.equals(y)debe regresar truesolo si y.equals(x)regresa true.
  • Transitividad
  • para cualquier valor dado y x, si regresa y regresa , debe devolver el valor . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Consistencia
  • para cualquier valor dado, xy yla llamada repetida x.equals(y)devolverá el valor de la llamada anterior a este método, siempre que los campos utilizados para comparar los dos objetos no cambien entre llamadas.
  • Comparación nula
  • para cualquier valor dado xla llamada x.equals(null)debe devolver false.

es igual a violación del contrato

Muchas clases, como las del Java Collections Framework, dependen de la implementación del método equals(), por lo que no debes descuidarlo, porque La violación del contrato de este método puede conducir a un funcionamiento irracional de la aplicación y, en este caso, será bastante difícil encontrar el motivo. Según el principio de reflexividad , todo objeto debe ser equivalente a sí mismo. Si se viola este principio, cuando agregamos un objeto a la colección y luego lo buscamos usando el método, contains()no podremos encontrar el objeto que acabamos de agregar a la colección. La condición de simetría establece que dos objetos cualesquiera deben ser iguales independientemente del orden en que se comparen. Por ejemplo, si tiene una clase que contiene solo un campo de tipo cadena, será incorrecto comparar equalseste campo con una cadena en un método. Porque en el caso de una comparación inversa, el método siempre devolverá el valor false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
De la condición de transitividad se deduce que si dos de tres objetos son iguales, entonces en este caso los tres deben ser iguales. Este principio puede violarse fácilmente cuando es necesario ampliar una determinada clase base añadiéndole un componente significativo . Por ejemplo, a una clase Pointcon coordenadas xy ynecesitas agregar el color del punto expandiéndolo. Para hacer esto, deberá declarar una clase ColorPointcon el campo apropiado color. Por lo tanto, si en la clase extendida llamamos al equalsmétodo padre, y en el padre asumimos que solo se comparan las coordenadas xy y, entonces dos puntos de diferentes colores pero con las mismas coordenadas se considerarán iguales, lo cual es incorrecto. En este caso, es necesario enseñar a la clase derivada a distinguir colores. Para hacer esto, puedes usar dos métodos. Pero uno violará la regla de simetría y el segundo, la transitividad .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
En este caso, la llamada point.equals(colorPoint)devolverá el valor truey la comparación colorPoint.equals(point)devolverá false, porque espera un objeto de “su” clase. Por tanto, se viola la regla de simetría. El segundo método implica realizar una verificación "ciega" en el caso de que no haya datos sobre el color del punto, es decir, tenemos la clase Point. O comprobar el color si hay información disponible sobre él, es decir, comparar un objeto de la clase ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
El principio de transitividad se viola aquí de la siguiente manera. Digamos que hay una definición de los siguientes objetos:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Así, aunque la igualdad p1.equals(p2)y sea cierta p2.equals(p3), p1.equals(p3)devolverá el valor false. Al mismo tiempo, el segundo método, en mi opinión, parece menos atractivo, porque En algunos casos, el algoritmo puede estar cegado y no realizar la comparación por completo, y es posible que usted no lo sepa. Un poco de poesía En general, según tengo entendido, no existe una solución concreta a este problema. Existe la opinión de un autor autorizado llamado Kay Horstmann de que se puede reemplazar el uso del operador instanceofcon una llamada a un método getClass()que devuelve la clase del objeto y, antes de comenzar a comparar los objetos, asegurarse de que sean del mismo tipo. , y no preste atención al hecho de su origen común. Por tanto, se cumplirán las reglas de simetría y transitividad . Pero al mismo tiempo, al otro lado de la barricada se encuentra otro autor, no menos respetado en amplios círculos, Joshua Bloch, que cree que este enfoque viola el principio de sustitución de Barbara Liskov. Este principio establece que "el código de llamada debe tratar una clase base de la misma manera que sus subclases sin saberlo " . Y en la solución propuesta por Horstmann, este principio se viola claramente, ya que depende de la implementación. En definitiva, está claro que el asunto es oscuro. También cabe señalar que Horstmann aclara la regla para aplicar su enfoque y escribe en un lenguaje sencillo que es necesario decidir una estrategia al diseñar clases, y si las pruebas de igualdad las llevará a cabo solo la superclase, puede hacerlo realizando la operacion instanceof. De lo contrario, cuando la semántica de la verificación cambie según la clase derivada y la implementación del método deba moverse hacia abajo en la jerarquía, debe usar el método getClass(). Joshua Bloch, a su vez, propone abandonar la herencia y utilizar la composición de objetos incluyendo una ColorPointclase en la clase Pointy proporcionando un método de acceso asPoint()para obtener información específica sobre el punto. Esto evitará romper todas las reglas, pero, en mi opinión, hará que el código sea más difícil de entender. La tercera opción es utilizar la generación automática del método igual utilizando el IDE. La idea, por cierto, reproduce la generación de Horstmann, lo que permite elegir una estrategia para implementar un método en una superclase o en sus descendientes. Finalmente, la siguiente regla de coherencia establece que incluso si los objetos xno ycambian, llamarlos nuevamente x.equals(y)debe devolver el mismo valor que antes. La regla final es que ningún objeto debe ser igual a null. Aquí todo está claro null: esto es incertidumbre, ¿el objeto es igual a incertidumbre? No está claro, es decir false.

Algoritmo general para determinar iguales.

  1. Verifique la igualdad de referencias de objetos thisy parámetros de métodos o.
    if (this == o) return true;
  2. Compruebe si el enlace está definido o, es decir, si lo está null.
    Si en el futuro, al comparar tipos de objetos, se utilizará el operador instanceof, este elemento se puede omitir, ya que este parámetro falseen este caso devuelve null instanceof Object.
  3. Compare tipos de objetos thisutilizando oun operador instanceofo método getClass(), guiado por la descripción anterior y su propia intuición.
  4. Si un método equalsse anula en una subclase, asegúrese de realizar una llamadasuper.equals(o)
  5. Convierta el tipo de parámetro oa la clase requerida.
  6. Realice una comparación de todos los campos de objetos importantes:
    • para tipos primitivos (excepto floaty double), usando el operador==
    • para campos de referencia necesitas llamar a su métodoequals
    • para matrices, puede usar la iteración cíclica o el métodoArrays.equals()
    • para tipos floaty doublees necesario utilizar métodos de comparación de las clases contenedoras correspondientes Float.compare()yDouble.compare()
  7. Y finalmente, responda tres preguntas: ¿el método implementado es simétrico ? ¿ Transitivo ? Acordado ? Los otros dos principios ( reflexividad y certeza ) suelen realizarse de forma automática.

Reglas de anulación de HashCode

Un hash es un número generado a partir de un objeto que describe su estado en algún momento. Este número se utiliza en Java principalmente en tablas hash como HashMap. En este caso, la función hash para obtener un número basado en un objeto debe implementarse de tal manera que garantice una distribución relativamente uniforme de elementos en la tabla hash. Y también para minimizar la probabilidad de colisiones cuando la función devuelve el mismo valor para diferentes claves.

Código hash del contrato

Para implementar una función hash, la especificación del lenguaje define las siguientes reglas:
  • llamar a un método hashCodeuna o más veces en el mismo objeto debe devolver el mismo valor hash, siempre que los campos del objeto involucrados en el cálculo del valor no hayan cambiado.
  • llamar a un método hashCodeen dos objetos siempre debe devolver el mismo número si los objetos son iguales (llamar a un método equalsen estos objetos devuelve true).
  • llamar a un método hashCodeen dos objetos desiguales debe devolver valores hash diferentes. Aunque este requisito no es obligatorio, se debe considerar que su implementación tendrá un efecto positivo en el rendimiento de las tablas hash.

Los métodos iguales y hashCode deben anularse juntos

Según los contratos descritos anteriormente, se deduce que al anular el método en su código equals, siempre debe anular el método hashCode. Dado que, de hecho, dos instancias de una clase son diferentes porque se encuentran en áreas de memoria diferentes, deben compararse según algunos criterios lógicos. En consecuencia, dos objetos lógicamente equivalentes deben devolver el mismo valor hash. ¿Qué sucede si solo se anula uno de estos métodos?
  1. equalshashCodeNo

    Digamos que definimos correctamente un método equalsen nuestra clase y hashCodedecidimos dejar el método como está en la clase Object. Entonces, desde el punto de vista del método, equalslos dos objetos serán lógicamente iguales, mientras que desde el punto de vista del método hashCodeno tendrán nada en común. Y así, al colocar un objeto en una tabla hash, corremos el riesgo de no recuperarlo mediante clave.
    Por ejemplo, así:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Evidentemente, el objeto que se coloca y el objeto que se busca son dos objetos diferentes, aunque lógicamente son iguales. Pero porque tienen valores hash diferentes porque violamos el contrato, podemos decir que perdimos nuestro objeto en algún lugar de las entrañas de la tabla hash.

  2. hashCodeequalsNo.

    ¿Qué sucede si anulamos el método hashCodey equalsheredamos la implementación del método de la clase Object? Como sabes, el equalsmétodo predeterminado simplemente compara punteros con objetos, determinando si se refieren al mismo objeto. Supongamos que hashCodehemos escrito el método de acuerdo con todos los cánones, es decir, lo hemos generado usando el IDE y devolverá los mismos valores hash para objetos lógicamente idénticos. Obviamente, al hacerlo ya hemos definido algún mecanismo para comparar dos objetos.

    Por tanto, en teoría debería llevarse a cabo el ejemplo del párrafo anterior. Pero todavía no podremos encontrar nuestro objeto en la tabla hash. Aunque estaremos cerca de esto, porque como mínimo encontraremos una canasta de tabla hash en la que se ubicará el objeto.

    Para buscar con éxito un objeto en una tabla hash, además de comparar los valores hash de la clave, también se utiliza la determinación de la igualdad lógica de la clave con el objeto buscado. Es decir, equalsno hay forma de hacerlo sin anular el método.

Algoritmo general para determinar el código hash

Aquí, me parece, no deberías preocuparte demasiado y generar el método en tu IDE favorito. Porque todos estos cambios de bits hacia la derecha y hacia la izquierda en busca de la proporción áurea, es decir, la distribución normal, esto es para tipos completamente tercos. Personalmente, dudo que pueda hacerlo mejor y más rápido que la misma Idea.

En lugar de una conclusión

Así, vemos que los métodos equalsjuegan hashCodeun papel bien definido en el lenguaje Java y están diseñados para obtener la igualdad lógica característica de dos objetos. En el caso del método, equalsesto tiene una relación directa con la comparación de objetos, en el caso de hashCodeuno indirecto, cuando es necesario, digamos, determinar la ubicación aproximada de un objeto en tablas hash o estructuras de datos similares para poder aumentar la velocidad de búsqueda de un objeto. Además de los contratos , equalsexiste hashCodeotro requisito relacionado con la comparación de objetos. Ésta es la coherencia de un método compareTode interfaz Comparablecon un archivo equals. Este requisito obliga al desarrollador a devolver siempre x.equals(y) == truecuando x.compareTo(y) == 0. Es decir, vemos que la comparación lógica de dos objetos no debe contradecirse en ninguna parte de la aplicación y siempre debe ser coherente.

Fuentes

Java efectivo, segunda edición. Josué Bloch. Traducción gratuita de un muy buen libro. Java, la biblioteca de un profesional. Volumen 1. Conceptos básicos. Kay Horstmann. Un poco menos de teoría y más práctica. Pero no todo está analizado con tanto detalle como el de Bloch. Aunque hay una opinión sobre los mismos iguales(). Estructuras de datos en imágenes. HashMap Un artículo extremadamente útil sobre el dispositivo HashMap en Java. En lugar de mirar las fuentes.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION