Java equals() y hashCode()
Aprende a sobreescribir correctamente equals() y hashCode() en Java para soportar colecciones y la igualdad basada en valores.
equals y hashCode son los dos métodos de Object en los que las colecciones basadas en hash — HashMap, HashSet, LinkedHashMap, cualquier estructura respaldada por hashing — confían silenciosamente. Impleméntalos correctamente y tus objetos se comportarán como valores: set.contains(point) encontrará el punto sin importar qué instancia de new Point(3, 4) pases. Impleméntalos mal y obtendrás duplicados en conjuntos, claves ausentes en mapas y errores que solo aparecen bajo carga.
El comportamiento predeterminado heredado de Object compara identidad: dos referencias son iguales solo cuando apuntan al mismo objeto. Eso está bien para cosas como conexiones de base de datos, donde cada instancia es un recurso distinto. Para clases similares a valores — dinero, puntos, nombres, fechas — casi siempre querrás igualdad de contenido en su lugar, y eso significa sobreescribir ambos métodos juntos.
El contrato
equals debe satisfacer cuatro reglas:
- Reflexiva —
x.equals(x)es verdadero. - Simétrica —
x.equals(y)si y solo siy.equals(x). - Transitiva — si
x.equals(y)yy.equals(z), entoncesx.equals(z). - Consistente — llamadas repetidas con campos sin modificar devuelven la misma respuesta.
Además: x.equals(null) debe retornar false, nunca lanzar una excepción.
hashCode tiene una regla que lo vincula a equals:
- Los objetos iguales deben tener códigos hash iguales. Los objetos desiguales pueden compartir un código hash (las colisiones están permitidas, aunque son malas para el rendimiento).
Esa única regla es la razón por la que no puedes sobreescribir uno sin el otro. Si a.equals(b) pero a.hashCode() != b.hashCode(), HashSet los coloca en cubetas diferentes, contains encuentra la incorrecta y tienes un duplicado fantasma.
Observa cómo se rompe el contrato
Esta clase sobreescribe equals pero olvida hashCode, por lo que hereda el hash basado en identidad de Object. Los dos objetos son "iguales" pero caen en cubetas diferentes — contains no puede encontrar el que acabas de agregar:
equals dice que los objetos son el mismo, pero el conjunto no puede encontrar el segundo. Sobreescribe hashCode para que coincida y la búsqueda tendrá éxito.
Anatomía de un equals correcto
Un equals funcional sigue una forma estándar:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}Paso a paso:
- Cortocircuito por identidad.
this == ocaptura el caso común rápidamente. - Verificación de tipo con vinculación.
instanceof Point prechaza nulos y tipos incorrectos en una sola expresión y vincula la referencia reducida. - Comparación de campos. Usa
==para primitivos,Objects.equals(a, b)para referencias que pueden ser nulas,Float.compare/Double.comparepara flotantes.
Objects.hash(...) construye un hash a partir de una lista de campos. Es ligeramente más lento que el código XOR/multiplicación escrito a mano, pero es correcto e inequívoco.
¿getClass o instanceof?
Dos escuelas:
instanceofpermite que una instancia de subclase sea igual a una instancia padre si el conjunto de campos de comparación es el mismo. Ligeramente más flexible.getClass()exige la clase en tiempo de ejecución exacta. Más fácil de mantener simétrico a través de jerarquías, pero rompe la sustituibilidad.
Para la mayoría de las clases de estilo valor, la forma más sencilla es hacer la clase final y usar instanceof. Sin final, mezclar los dos estilos a lo largo de una jerarquía es donde viven la mayoría de los errores de igualdad. Los records esquivan la decisión por completo — son implícitamente finales y el equals generado usa una verificación de tipo exacto.
Campos de punto flotante
No uses == en campos double o float — el valor +0.0 es igual a -0.0 con ==, pero Double.compare los trata de forma diferente, y NaN == NaN es false. Double.compare(a, b) == 0 y Float.compare dan la respuesta consistente que requiere el contrato.
Arrays
Object.equals en un array compara referencias, no contenidos. Usa Arrays.equals(a, b) para arrays unidimensionales, Arrays.deepEquals para multidimensionales. De manera similar, usa Arrays.hashCode / Arrays.deepHashCode en hashCode.
La mutabilidad es hostil para las colecciones basadas en hash
Si mutes un campo que forma parte de equals/hashCode después de insertar el objeto en un HashSet, la cubeta donde el conjunto lo colocó deja de coincidir con el nuevo hash — y el objeto se vuelve inalcanzable mediante contains. La regla más segura: los campos usados en equals deben ser final. Si eso no es posible, nunca insertes el objeto en una colección basada en hash.
No escribas ninguno a mano para clases de datos simples
Si la clase es un portador de datos puro, prefiere un record — el compilador genera equals y hashCode correctos por ti, y los dos siempre estarán sincronizados a medida que los campos cambien. Si no puedes usar un record, el comando "generar equals/hashCode" de tu IDE es la siguiente mejor opción.
Un ejemplo completo
Qué sigue
equals permite que tus objetos se comparen a sí mismos; toString les permite describirse a sí mismos. El siguiente capítulo trata sobre sobreescribir toString para producir una salida que sea realmente útil en registros, mensajes de error y depuradores. Continúa en Método toString de Java.