1. Introducción
Cómo comparamos objetos
En Java los objetos no son solo datos: cada uno tiene su propia dirección en memoria. El operador == responde a la pregunta «¿es el mismo objeto?», es decir, compara referencias (direcciones), no el contenido. El método equals está pensado para comparar el contenido. Por defecto, si no se sobrescribe, equals se comporta como ==.
Person p1 = new Person("Iván", 20);
Person p2 = new Person("Iván", 20);
System.out.println(p1 == p2); // false — ¡son objetos distintos en memoria!
Para que dos objetos distintos se consideren iguales por sus datos (por ejemplo, si coinciden todos los campos significativos), es necesario sobrescribir equals. Y si planeas usar los objetos en colecciones con hash, debes sobrescribir correctamente junto con él hashCode.
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // es el mismo objeto
if (o == null || getClass() != o.getClass()) return false; // comprobamos la clase
Person person = (Person) o; // convertimos al tipo necesario
return age == person.age && name.equals(person.name); // comparamos campos
}
@Override
public int hashCode() {
return Objects.hash(name, age); // para que funcione con HashSet/HashMap
}
}
Esta sobrescritura es crítica para el funcionamiento correcto con HashSet/HashMap: sin equals y hashCode las colecciones tratarán incluso objetos con los mismos datos como distintos.
La relación de equivalencia en equals depende de tus requisitos: puedes comparar por todos los campos, por parte de ellos (por ejemplo, solo el email en User) — lo importante es la consistencia y el cumplimiento del contrato.
¿Dónde es especialmente importante?
- En colecciones basadas en tablas hash: HashSet, HashMap, LinkedHashSet, etc.
- Al buscar y eliminar elementos en colecciones: sin un equals correcto, el objeto deseado puede «no encontrarse».
- En la lógica de negocio: por ejemplo, dos User con el mismo email deben considerarse el mismo usuario.
hashCode: ¿para qué sirve?
Las colecciones con hash (por ejemplo, HashSet, HashMap) usan tablas hash. El método hashCode calcula un entero — la «dirección del bucket» donde caerá el objeto. Si dos objetos son iguales según equals, sus hashCode deben coincidir. Si rompes esta regla, las colecciones empezarán a comportarse de forma impredecible.
2. Contrato de equals y hashCode
Contrato de equals
Requisitos básicos para el comportamiento de equals:
- Reflexividad: a.equals(a) siempre es true.
- Simetría: si a.equals(b) es true, entonces b.equals(a) también es true.
- Transitividad: si a.equals(b) y b.equals(c), entonces a.equals(c) también es true.
- Consistencia: mientras los objetos no cambien, el resultado de las invocaciones es estable.
- Comparación con null: cualquier objeto no es igual a null.
Contrato de hashCode
- Si dos objetos son iguales según equals, sus hashCode son iguales.
- Si los objetos no son iguales, sus códigos hash pueden coincidir (las colisiones son posibles, aunque indeseables).
- Mientras el objeto no cambie lógicamente, su hashCode debe permanecer constante.
En otras palabras, que coincida el hashCode es una condición necesaria, pero no suficiente, para la igualdad: un mismo hash no garantiza igualdad según equals.
3. Implementación de equals y hashCode: ejemplo
Consideremos la clase Person, donde la igualdad se define por los campos name y age.
public class Person {
private String name;
private int age;
// Constructor, getters, setters...
@Override
public boolean equals(Object o) {
if (this == o) return true; // Comparación de referencias
if (o == null || getClass() != o.getClass()) return false; // Comprobación de clase
Person person = (Person) o; // Conversión de tipo
// Comparamos campos
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 — una elección común de número primo
return result;
}
}
- Primero, comprobaciones rápidas: referencia y clase.
- Después, comparación de los campos significativos.
- En hashCode se usa el número primo 31 para reducir colisiones.
Uso de Objects.equals y Objects.hash
Desde Java 7, la clase Objects simplifica el código y lo hace más seguro frente a null:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals, hashCode y compareTo: cómo se relacionan
¿Cómo se relacionan equals y compareTo?
La interfaz Comparable define el método compareTo, que devuelve un número negativo/cero/positivo para «menor/igual/mayor». Es deseable que de a.compareTo(b) == 0 se siga a.equals(b). Lo contrario no es obligatorio.
Si rompes la consistencia (por ejemplo, compareTo compara solo por edad, y equals por nombre y edad), entonces las colecciones ordenadas como TreeSet/TreeMap pueden comportarse de manera inesperada: los objetos se consideran «iguales» desde el punto de vista del orden, pero no iguales por contenido.
equals y hashCode en colecciones
- En HashSet y HashMap las operaciones de añadir/buscar/eliminar dependen de una implementación correcta de equals y hashCode.
- Sin sobrescritura, estas colecciones consideran los objetos «distintos», incluso si sus datos son iguales.
5. Ejemplos: cómo funciona en las colecciones
HashSet: almacenamiento de objetos únicos
Set<Person> people = new HashSet<>();
people.add(new Person("Iván", 20));
people.add(new Person("Iván", 20)); // Duplicado
System.out.println(people.size()); // 1, si equals/hashCode están implementados correctamente
Sin un equals/hashCode correctos, ambos objetos entrarán en el conjunto.
HashMap: búsqueda por clave
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Ana", 25);
Person p2 = new Person("Ana", 25);
map.put(p1, "Usuario 1");
System.out.println(map.get(p2)); // "Usuario 1", si equals/hashCode están implementados correctamente
Sin el contrato, la colección devolverá null — para ella son claves «distintas».
6. Buenas prácticas: consejos de implementación
- Incluye en equals/hashCode todos los campos que determinen la «identidad» del objeto.
- No uses campos mutables (que cambian tras añadir a colecciones) al calcular el hashCode.
- Deja que la IDE genere los métodos — menor probabilidad de errores tipográficos.
- En equals comprueba primero this == o, luego la clase y después los campos.
- Para comparar campos que son objetos, usa Objects.equals.
- Para el código hash usa Objects.hash o un patrón probado con el factor 31.
7. Detalles útiles
¿Por qué no se puede usar solo hashCode?
Las colisiones son inevitables: objetos distintos pueden tener el mismo hashCode. El hash es solo una guía rápida para el «bucket»; la decisión final sobre la igualdad la toma equals.
¿Se puede no sobrescribir equals y hashCode?
Solo si estás seguro de que los objetos nunca se compararán por contenido ni se usarán como claves/elementos únicos en colecciones. En la práctica, esto es raro.
Diferencia entre ==, equals y compareTo
| Operador/método | ¿Qué compara? | ¿Para qué sirve? |
|---|---|---|
|
Referencias (direcciones en memoria) | Comprobar «¿es el mismo objeto?» |
|
Contenido de los objetos | Igualdad según la lógica de negocio |
|
Orden (menor/igual/mayor) | Ordenación |
8. Errores típicos al implementar equals y hashCode
Error n.º 1: sobrescribir equals y olvidar hashCode. Los objetos se consideran iguales, pero caen en buckets distintos de la tabla hash: se estropean la búsqueda y la eliminación.
Error n.º 2: usar campos mutables en hashCode. Si un campo cambia después de añadir a la colección, el objeto se «pierde»: el hash cambia, pero el bucket no.
Error n.º 3: romper la simetría/transitividad de equals. a.equals(b) da true, pero b.equals(a) da false, o se rompe la transitividad — las colecciones empiezan a comportarse de forma impredecible.
Error n.º 4: no comprobar la clase en equals. Comparar objetos de clases distintas lleva a resultados incorrectos o a excepciones.
Error n.º 5: comparar cadenas y objetos con ==. El operador == compara referencias; usa equals para el contenido.
Error n.º 6: falta de consistencia entre compareTo y equals. Si a.compareTo(b) == 0, pero !a.equals(b), las colecciones TreeSet/TreeMap pueden considerar elementos iguales para el orden, pero distintos en igualdad — origen de «fantasmas» y duplicados.
GO TO FULL VERSION