CodeGym /Cursos /JAVA 25 SELF /Contratos de equals y hashCode

Contratos de equals y hashCode

JAVA 25 SELF
Nivel 29 , Lección 0
Disponible

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?»
equals
Contenido de los objetos Igualdad según la lógica de negocio
compareTo
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.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION