DEV Community

Elías Canales
Elías Canales

Posted on

Método equals() en Java

Claves para definir la igualdad lógica entre objetos

Es un problema que pues encontrar fácilmente cuando programas en Java, probablemente con la clase String, y es que si quieres comparar Objetos necesitas equals() porque si usas ‘==’ lo que estás comparando es la referencia.

//Valor NO tiene implementado el método equals()
final Valor a = new Valor(1);
assertTrue(a == a && a.equals(a));

final Valor b = new Valor(1);
assertFalse(a == b);
assertFalse(a.equals(b));

//Valor tiene implementado el método equals()
final Valor a = new Valor(1);
assertTrue(a == a && a.equals(a));

final Valor b = new Valor(1);
assertFalse(a == b);
assertTrue(a.equals(b));
Enter fullscreen mode Exit fullscreen mode

Cuando no tenemos implementado el método equals() la comparación ‘==’ tiene el mismo resultado. Es decir, estamos comparando la referencia. Pero si implementamos el método equals() ya es diferente. Ya tendremos la comparación de valor y referencia, separadas.

Antes de adentrarnos en cómo debe ser un método equals, podemos empezar hablando de cuando deberíamos de sobrescribir este método.

  1. Cuando la clase sea una clase valor, es decir, cuando está representando algo que por sus atributos podemos decir, si es o no igual a otro objeto.
  2. Cuando la instancia por sí misma no es única. Por ejemplo, una clase de un servicio en principio es única, si queremos saber si es la misma podemos usar ‘==’ porque no tiene propiedades que la hagan única frente a otra.
  3. Cuando la superclase no tiene un método equals(). Para evitar romper la propiedad transitiva (más adelante hablaremos de esto).

Una vez tenemos claro que queremos un método equals(), lo siguiente es empezar a sobrescribirlo haciendo uso de la anotación @Override, esto es importante porque nos garantiza que realmente estamos sobrescribiendo.

Ya que si no estamos sobrescribiendo nada, tendremos una alerta por parte del IDE, o en tiempo de compilación.
Por ejemplo, si en vez de hacer equals(Object o) hacemos equals(Valor v).

Claramente no estamos sobrescribiendo el método equals() y podríamos tener resultados no deseados, por ello, mejor utilizar la anotación @Override en estos casos.

Si vamos a la documentación sobre este método vamos a ver las propiedades que debería de tener un método equals().

Reflexiva: Debe ser igual asimismo, obviamente el mismo objeto con los mismo valores debe decirnos que es igual (a.equals(a)).
Simétrica: No importa el orden que comparemos, es decir, debe ser lo mismo decir que ‘a’ es igual a ‘b’, que ‘b’ es igual a ‘a’ (a.equals(b) && b.equals(a) == true).
Transitiva: Si tenemos 3 instancias ‘a’,’b’ y ‘c’, si sabemos que a == b y
b == c, entonces a == c (a.equals(b) && b.equals(c) && a.equals(c)).
Consistente: Si llamamos varias veces a a.equals(b) y no hemos cambiado ninguno de los dos objetos, el resultado siempre debería ser el mismo.
Comparado contra un null: Devolver falso cuando se compara con un nulo (a.equals(null) == false).

Como podemos ver todas las propiedades tienen lógica, no son nada raro. Pero aunque parezca que es difícil romper alguna de esas propiedades, lo cierto es que hay algunas situaciones que pueden llevarnos a romperlo.

Romper simetría

@Override
public boolean equals(Object o) {
    if(o instanceof Valor)
        return num.equals(((Valor) o).num);
    else if(o instanceof Integer)
        return num.equals(o);
    else
        return false;
}
Enter fullscreen mode Exit fullscreen mode

Una forma de romper la simetría es tratar dos tipos dentro de un equals(), de hecho es uno de los motivos para no hacer un equals(), porque si la clase que estas creando hereda de otra, y la superclase ya tiene una definición del método equals, en ese caso no lo sobrescribas.

Fíjate en el ejemplo, está claro que si comparamos dos objetos Valor va a funcionar. Si comparamos un Valor con un Integer también, pero y al reves que pasaría.

final Valor a = new Valor(1);
final Integer b = 1;

assertTrue(a.equals(b));
assertFalse(b.equals(a));
Enter fullscreen mode Exit fullscreen mode

Como vemos, ‘a’ sí es igual a ‘b’, pero ‘b’ no es igual a ‘a’. Es algo obvio, pero tenemos que tener cuidado de no querer cubrir dos tipos en un equals(), vamos a ver que algo muy similar que pasa para el problema transitivo.

Romper transitividad

Tenemos la clase Valor en la cual implementamos el equals() solo para la clase Valor. Luego hemos creado una clase que hereda de Valor, que hemos llamado ValorAvanzado.

Si no tenemos en cuenta Valor en ValorAvanzado, estaríamos rompiendo simetría, porque tenemos que tener en cuenta que un ValorAvanzado también es un Valor.

//Valor
@Override
public boolean equals(Object o) {
    if(o instanceof Valor)
        return num.equals(((Valor) o).num);
    else
        return false;
}

//ValorAvanzado
@Override
public boolean equals(Object o) {
    if(o instanceof ValorAvanzado)
        return super.equals(o) && 
          decimals.equals(((ValorAvanzado) o).decimals);
    else if(o instanceof Valor)
        return super.equals(o);
    else
        return false;
}

final Valor a = new ValorAvanzado(1, 2);
final Valor b = new Valor(1);
final Valor c = new ValorAvanzado(1, 3);

assertTrue(a.equals(b));
assertTrue(b.equals(c));
assertFalse(a.equals(c));
Enter fullscreen mode Exit fullscreen mode

Aunque ‘a’ es igual a ‘b’, y ‘b’ es igual a ‘c’, resulta que ‘a’ no es igual a ‘c’ en este caso. Por tanto, hemos roto la simetría. Como decía antes es muy similar al anterior, y es que cuando queremos abarcar dos tipos en un equals() nos vamos a encontrar problemas.

Este caso, concretamente, dado que la superclase ya tiene definido el método equals() no debemos sobrescribirlo. En caso de hacerlo tenemos que saber las consecuencias, y rompiendo alguna de las propiedades que la propia documentación establece, puede llevarnos a situaciones no deseadas.

La mejor solución en este caso, es hacer uso de la composición en vez de la herencia. Con la cual, podríamos tener un método equals() funcional para ambas clases.

Romper consistencia

La consistencia se puede romper por las operaciones que hagamos en el propio método equals(). Por ejemplo, si tenemos que hacer una llamada a un servicio externo, eso podría dar resultados no deterministas, y por tanto, romper la consistencia. Debemos solo hacer uso de elementos que están en memoria y nos permitirán tener resultados deterministas.

Lombok

En Lombok hay una anotación para definir automáticamente el equals y el hashCode (@EqualsAndHashCode), ya que cuando se sobrescribe el equals debes sobrescribir el hashCode. Pero debes tener en cuenta que la anotación de Lombok no va a arreglar todos los problemas que hemos comentado, sobre todo los relativos a herencia.

Referencias
https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Object.html
https://projectlombok.org
Joshua Bloch, Effective Java (3ª edición), Addison-Wesley, 2018.

Top comments (0)