El contrato de equals/hashCode
Por qué ambos deben coincidir
El contrato de equals/hashCode es una lección gratuita de Java Academy en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Java Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Java Academy incluye 4 lecciones en total.
Dos métodos, un contrato
Todo objeto de Java hereda equals() y hashCode() de Object.
Cuando sobrescribe uno, casi siempre debe sobrescribir también el otro. Ambos forman un contrato vinculante del que dependen las colecciones basadas en hash.
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}La regla fundamental
El contrato establece lo siguiente:
- Si
a.equals(b)es true, entoncesa.hashCode()debe ser igual ab.hashCode(). - Si los códigos hash difieren, los objetos son necesariamente distintos.
Los objetos iguales deben tener códigos hash iguales. No se exige lo contrario.
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}Los objetos distintos pueden compartir un hash
Dos objetos distintos pueden compartir un código hash. Eso es simplemente una colisión, y el mapa la gestiona con equals().
Por tanto, la única situación prohibida es que haya objetos iguales con códigos hash distintos.
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}Una clase que solo sobrescribe equals
Esta es una clase defectuosa. Sobrescribe equals(), pero no hashCode().
Ahora dos puntos lógicamente iguales tienen códigos hash distintos heredados de Object, lo que infringe el contrato.
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}El error en un HashSet
La clase defectuosa falla en un HashSet. El conjunto busca en el bucket elegido por el hash incorrecto y nunca encuentra el elemento igual.
El resultado son duplicados que deberían haberse rechazado.
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}Cómo corregirlo
Sobrescriba ambos métodos y derívelos de los mismos campos. Así, los objetos iguales compartirán un hash y llegarán al mismo bucket.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}equals debe ser reflexivo
Un equals() correcto cumple cuatro propiedades. La primera es ser reflexivo: x.equals(x) debe ser true.
Puede parecer obvio, pero la lógica personalizada puede incumplirlo si se olvida de gestionar el caso de la misma referencia.
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}Simétrico y transitivo
Hay otras dos reglas:
- Simétrico: si
a.equals(b), entoncesb.equals(a). - Transitivo: si
a.equals(b)yb.equals(c), entoncesa.equals(c).
Combinar tipos de una jerarquía de clases suele romper la simetría, por lo que debe comparar las clases cuidadosamente.
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}Consistente
La cuarta regla es ser consistente: las llamadas repetidas devuelven el mismo resultado mientras los objetos no cambien.
Por eso debe basar equals y hashCode en campos inmutables. Modificar después de la inserción un campo utilizado en hashCode corrompe el mapa.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}Los records lo hacen por usted
Un record de Java genera automáticamente equals() y hashCode() a partir de todos sus componentes.
Para datos que representan valores, prefiera los records. El contrato se cumple automáticamente y permanece sincronizado.
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}La trampa de las claves mutables
Si modifica un campo utilizado en hashCode después de introducir el objeto en un mapa, ya no podrá encontrarlo.
El mapa busca en el bucket antiguo; ahora el objeto genera un hash que apunta a otro. La entrada se convierte en un fantasma perdido.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}Comprobación rápida
Compruebe su comprensión del contrato.
Repaso
Ha aprendido el contrato de equals/hashCode:
- Los objetos iguales deben tener códigos hash iguales.
- equals debe ser reflexivo, simétrico, transitivo y consistente.
- Base ambos métodos en los mismos campos inmutables.
- Los records generan implementaciones correctas automáticamente.
A continuación, escribirá su propio hashCode correcto a mano.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}Preguntas frecuentes
¿La lección «El contrato de equals/hashCode» es gratis?
Sí — el texto completo de «El contrato de equals/hashCode» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Java Academy, actualiza a CoddyKit PRO. El curso de Java Academy incluye 4 lecciones en total.
¿Qué aprenderé en «El contrato de equals/hashCode»?
Por qué ambos deben coincidir Practicas Java Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar Java Academy?
No se requiere experiencia previa. Java Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.
¿Cuánto tiempo toma la lección «El contrato de equals/hashCode»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de Java Academy?
Sí. Cada lección de Java Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Cómo funciona HashMap
- El contrato de equals/hashCode
- Implementación de hashCode
- Treeification y rendimiento