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

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente 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 sobrescribamos el método equals también debemos sobrescribir 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) 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).

Hay dos formas comunes de implementar el método equals, una más restrictiva pero que cumple las propiedades y otra que no cumple completamente las propiedades pero es de utilidad en ciertos casos. Son las siguientes en las que cambia la sentencia que comprueba el tipo de la instancia del objeto con el que se está evaluando la igualdad. En el artículo How to Implement Java’s equals Method Correctly están descritas las implicaciones y motivo de existir de ambas variantes además de explicar que garantiza cada sentencia del método equals.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PhoneNumber {

    private Integer lineNumber;
    private Integer prefix;
    private Integer areaCode;

    ...

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

        PhoneNumber that = (PhoneNumber) o;
        return super.equals(that)
            && Objects.equals(this.lineNumber, that.lineNumber)
            && Objects.equals(this.prefix, that.prefix)
            && Objects.equals(this.areaCode, that.areaCode);
    }
}
PhoneNumber-equals-1.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PhoneNumber {

    private Integer lineNumber;
    private Integer prefix;
    private Integer areaCode;

    ...

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber that = (PhoneNumber) o;
        return super.equals(that)
            && Objects.equals(this.lineNumber, that.lineNumber)
            && Objects.equals(this.prefix, that.prefix)
            && Objects.equals(this.areaCode, that.areaCode);
    }
}
PhoneNumber-equals-2.java

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.compare y en los objetos deberemos tener en cuenta si la referencia es posible que se a nula para evitar la excepción NullPointerException cosas que la clase EqualsBuilder ya tiene en cuenta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PhoneNumber {

    private Integer lineNumber;
    private Integer prefix;
    private Integer areaCode;

    ...

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber that = (PhoneNumber) o;
        return new EqualsBuilder()
            .appendSuper(super.equals(that))
            .append(this.lineNumber, that.lineNumber)
            .append(this.prefix, that.prefix)
            .append(this.areaCode, that.areaCode)
            .isEquals();
    }
}
PhoneNumber-equals-commons-lang.java

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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PhoneNumber {

    ...

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
}
PhoneNumber-hashcode.java

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.

1
2
3
4
5
6
7
8
9
public class PhoneNumber {

    ...

    @Override
    public int hashCode() {
        return Objects.hash(areaCode, prefix, lineNumber);
    }
}
PhoneNumber-hashcode-java.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PhoneNumber {

    ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31).
            append(areaCode).
            append(prefix).
            append(lineNumber).
            toHashCode();
    }
}
PhoneNumber-hashcode-commons-lang.java

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.


Comparte el artículo: