W3docs

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:

  • Reflexivax.equals(x) es verdadero.
  • Simétricax.equals(y) si y solo si y.equals(x).
  • Transitiva — si x.equals(y) y y.equals(z), entonces x.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:

java— editable, runs on the server

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 == o captura el caso común rápidamente.
  • Verificación de tipo con vinculación. instanceof Point p rechaza 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.compare para 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:

  • instanceof permite 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

java— editable, runs on the server

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.

Práctica

Práctica
¿Por qué es un error sobreescribir `equals` sin también sobreescribir `hashCode`?
¿Por qué es un error sobreescribir `equals` sin también sobreescribir `hashCode`?
Was this page helpful?