Cómo implementar correctamente y por qué los métodos equals y hashCode de los objetos Java

Publicado por pico.dev el , actualizado el .
blog-stack java planeta-codigo programacion
Comentarios

Los métodos equals y hashCode son esenciales en las colecciones de objetos. Para su correcta implementación es necesario conocer unas cuantas propiedades que han de cumplir estos métodos. Pueden parecer sencillos pero no lo son tanto y una mala implementación posiblemente produzca algún tipo de error o comportamiento anómalo indeseado. En el siguiente artículo comento varias formas de implementarlos de forma sencilla y correcta.

Java

En Java los métodos equals y hashCode están definidos en la raíz de la jerarquía de clases, esto es en la clase Object, lo que significa que todas las instancias de objetos los poseen. Estos métodos son especialmente importantes ya que afectan al correcto funcionamiento de las colecciones como Collection, List, Set y Map, colecciones, listas, conjuntos y mapas que es difícil que cualquier programa no use alguna implementación de ellas.

El método equals es usado en las colecciones de tipo List, Set, y también Map para determinar si un objeto ya está incluida en la colección, el método hashCode es usado en los Map para encontrar el objeto asociado a la clave. Dado que las colecciones son ampliamente usadas en cualquier programa la correcta implementación implementación de los métodos equals y hashCode es fundamental ya que de lo contrario descubriremos errores poco agradables.

Una de las cosas que tenemos que tener cuenta es que siempre que sobreescribamos el método equals también debemos sobreescribir el método hashCode. Según el contrato definido en la clase Object deberemos saber que:

  • Durante la ejecución del programa el método hashCode debe retornar el mismo valor siempre que no se modifique la información usada en el método equals.
  • Si dos objetos son iguales según sus métodos equals entonces el valor devuelto por hashCode en cada uno de los dos objetos debe devolver el mismo valor.
  • Si dos objetos son distintos según sus métodos equals el valor devuelto no ha de ser necesariamente distinto aunque se recomienda para mejorar el rendimiento de las colecciones Map.

Cómo implementar el método equals

Según la especificación del método equals definido en la clase Object debe tener las siguientes propiedades:

  • Es reflexiva: para cualquier referencia no nula de x, x.equals(x) debe retornar true.
  • Es simétrica: para cualquier referencia no nula de x e y, x.equals(y) debe retornar true si y solo si y.equals(x) retorna true.
  • Es transitiva: para cualquier referencia no nula de x, y y z, si x.equals(y) debe retorna true y y.equals(z) retorna true entonces x.equals(z) debe retornar true.
  • Es consistente: para cualquier referencia no nula de x e y, múltiples invocaciones de x.equals(y) consistentemente debe retornar true o false, si no se ha modificado la información utilizada en la comparación.
  • Para para cualquier referencia no nula de x, x.equals(null) debe retornar false.

La implementación del método equals de la clase Object usa la equivalencia más restrictiva posible, esto es, para cualquier referencia no nula de x e y este método retorna true si y solo si son el mismo objeto (x == y tienen la misma referencia).

Según estas reglas una implementación del método equals tiene la siguiente forma:

Usando la clase EqualsBuilder de la librería commons-lang la implementación es aparentemente similar pero en el caso de necesitar hacer comparaciones con datos de tipo float, double, arrays u objetos hace la implementación un poco más sencilla. En los float y double para hacer la comparación deberíamos usar los métodos Float.compare y Double.commpare y en los objetos deberemos tener en cuenta si la referencia es posible que se a nula para evitar la excepción NullPinterException cosas que la clase EqualsBuilder ya tiene en cuenta.

Como implementar el método hashCode

La implementación del método hashCode se debe realizar según los siguientes pasos:

  • Almacenar un valor constante distinto de 0 en una variable int, por ejemplo 17.
  • Por cada campo usado en el método equals se debe obtener un hash code (int) realizando:
    • Si el campo es un boolean se debe calcular (f ? 1 : 0).
    • Si el campo es un byte, char, short o int se debe calcular (int) f.
    • Si el campo es un long se debe calcular (int) (f ^ (f >>> 32)).
    • Si el campo es un float se debe calcular Float.floatToIntBits(f).
    • Si el campo es un double se debe calcular Double.doubleToLongBits(f) y calcular el hash del long obtenido en el paso para los tipos long.
    • Si el campo es una referencia a un objeto y el método equals de esta clase compara recursivamente invocando el método equals del campo, invocar su método hashCode. si el valor de campo es nulo se debe retornar una constante que tradicionalmente es 0.
    • Si el campo es un array se debe tratar individualmente cada elemento aplicando estas reglas a cada elemento. Si cada elemento del array es significativo se puede usar Arrays.hashCode.
    • Combinar los hash code obtenidos de la siguiente forma, result = 31 * result + c.

Implementar este método en cada clase de una aplicación es tedioso, repetitivo y propenso a errores, para hacer más sencilla su implementación existe el método Objects.hash desde la versión 7 de Java. Si usamos una versión anterior a Java 7 disponemos de la clase HashCodeBuilder en la librería commons-lang. La misma implementación anterior quedaría.

En el libro Effective Java se explican con un poco más detalle estas dos cosas y muchas otras otras sobre Java que son muy interesantes conocer, el libro es una buena y recomendada lectura para todo programador Java que está entre los 8+ libros para mejorar como programadores que recomiendo.

Yo apoyo al software libre