1. Generación automática de equals, hashCode, toString
¿Para qué sirven estos métodos?
Al trabajar con objetos en Java, enseguida te encuentras con las mismas tareas una y otra vez. A veces necesitas comprobar si dos objetos son iguales. Por ejemplo, averiguar si ya está en una colección como Set o Map. En otros casos, el objeto se usa como clave en una HashMap, y ahí hacen falta reglas de comparación específicas. Y casi siempre quieres imprimir un objeto en el log o en pantalla de forma que la salida no sea un galimatías tipo MyClass@7b23ec81, sino algo con sentido.
Para estos casos, cada clase en Java tiene tres métodos especiales:
- equals(Object o) se encarga de comprobar la igualdad.
- hashCode() proporciona una «huella» numérica, necesaria para colecciones como las tablas hash.
- toString() devuelve una representación de cadena legible del objeto, lo que facilita mucho la depuración y la impresión.
¿Por qué es doloroso en clases normales?
En las clases normales hay que escribir estos métodos a mano. Y ahí empiezan el tedio y el dolor de cabeza. Sale un montón de código plantilla (boilerplate) que solo llena la clase. Es muy fácil equivocarse en algo: olvidar comparar un campo, calcular mal el hashCode y luego cazar bugs misteriosos. Y si añades un campo nuevo, te toca volver a tocar todos estos métodos y reescribirlo todo.
Ejemplo de una clase normal
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
¿Te resulta familiar? Sí, ¡y eso que son solo dos campos! ¿Y si fueran veinte?
Cómo lo hace un record
Una clase record hace todo esto por ti. Basta con declarar:
public record Point(int x, int y) { }
Y Java generará automáticamente:
- Constructor
- Accesores (x(), y())
- equals, hashCode, toString
Métodos generados automáticamente
- equals compara todos los componentes del record por valor.
- hashCode se calcula a partir de todos los componentes.
- toString devuelve una cadena del tipo Point[x=1, y=2].
¡Veámoslo en acción!
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
Salida:
true
true
Point[x=1, y=2]
Todo funciona como se espera, ¡sin una sola línea extra de código!
2. Por qué importa: colecciones, depuración y seguridad
Funcionamiento correcto en colecciones
Imagina que usas objetos como claves en una HashMap o como elementos en un HashSet. Si equals y hashCode están mal implementados, las colecciones se comportarán de forma extraña: no encontrarán un elemento que acabas de añadir o, al revés, considerarán iguales dos objetos distintos.
Con las clases record puedes estar seguro: la comparación y el hash siempre tienen en cuenta todos los componentes del record (en el orden en que se declaran).
Ejemplo: usar un record como clave
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — ¡funciona!
}
}
Ten en cuenta: p1 y p2 son objetos distintos (referencias diferentes), pero contienen los mismos valores de campos, así que se consideran iguales. Y conocerás más sobre Map y HashMap en el nivel 26 :P
Comodidad para la depuración y el registro
En lugar de un apagado Point@1a2b3c4d (como ocurre por defecto con las clases normales), una clase record se imprime de forma clara e informativa:
Point[x=3, y=4]
Esto ahorra mucho tiempo al depurar y al registrar.
3. Cómo funcionan equals, hashCode y toString dentro de un record
Método equals
Una clase record implementa equals de forma que dos objetos se consideran iguales si:
- Son del mismo tipo (misma clase record)
- Todos sus componentes son iguales (== para primitivos, equals() para objetos)
Ejemplo de comparación
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
Método hashCode
El hash se calcula a partir de todos los componentes del record, normalmente usando el método estándar Objects.hash(...).
System.out.println(p1.hashCode()); // Por ejemplo, 994
System.out.println(p2.hashCode()); // También 994
System.out.println(p3.hashCode()); // Otro número
Método toString
La representación en cadena siempre tiene el formato:
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. Sobrescribir equals, hashCode y toString: ¿cuándo y cómo?
A veces (rara vez, pero ocurre) necesitas cambiar el comportamiento estándar de estos métodos. Por ejemplo, quieres que toString devuelva una cadena con otro formato, o que la comparación se haga solo por parte de los campos.
Atención: si sobrescribes equals/hashCode, ¡hazlo con mucho cuidado! Romper su «contrato» puede provocar bugs difíciles de detectar.
Cómo sobrescribir un método
Simplemente declara tu propio método dentro del cuerpo de la clase record:
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
¿Se pueden sobrescribir equals/hashCode?
Sí, pero no se recomienda en absoluto salvo que sepas bien lo que haces. Por ejemplo, si quieres que la comparación sea solo por el campo x (lo cual ya es raro):
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
Pero ten cuidado: si sobrescribes equals, sobrescribe siempre también hashCode; de lo contrario, las colecciones funcionarán de forma incorrecta.
Mejores prácticas
- Si no sabes exactamente para qué sobrescribir, ¡no sobrescribas!
- Para toString, puedes usar tu propio formato sin problema si lo prefieres.
- Para equals/hashCode, solo si hay un motivo de peso y entiendes las consecuencias.
5. Práctica: comparación de objetos y uso de records en colecciones
Ejemplo: comparar dos records
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
Ejemplo: usar un record como clave en HashMap
Imaginemos que tenemos una aplicación donde almacenamos el número de visitas de usuarios por su nombre y edad (por si acaso hubiera dos «Ivan de 20 años» en el club).
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// Comprobemos que la búsqueda por valor funciona correctamente
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
Si equals y hashCode no estuvieran bien implementados, la búsqueda no funcionaría. Más sobre Map y HashMap en las lecciones del nivel 26 :P
6. Errores típicos al trabajar con equals, hashCode y toString en clases record
Error n.º 1: Esperar que los campos se puedan cambiar tras la creación.
Los campos de un record siempre son final, y la comparación se hace por sus valores, que se establecen en el constructor. Si de algún modo «cambias» el estado interno (por ejemplo, a través de un objeto mutable dentro de un campo), la comparación y el hash pueden volverse incorrectos.
Error n.º 2: Sobrescribir equals, pero olvidarte de hashCode.
Si sobrescribes uno de estos métodos, ¡sobrescribe siempre el otro! En caso contrario, las colecciones (HashSet, HashMap) se comportarán de forma impredecible.
Error n.º 3: Esperar que toString use otro formato.
Si necesitas un formato de cadena específico, simplemente sobrescribe toString. Por defecto el formato siempre es ClassName[field1=value1, field2=value2].
Error n.º 4: Usar records para clases complejas con campos mutables.
Los campos de un record deben ser inmutables. Si usas como campo, por ejemplo, un ArrayList y alguien cambia su contenido, la comparación y el hash pueden «romperse». Para records es mejor usar solo tipos inmutables.
Error n.º 5: Usar records para clases cuyo comportamiento no es de value object.
Un record no es «una clase pequeña con sintaxis corta». Es un value object, diseñado para almacenar un conjunto de valores. Si tienes lógica compleja, estado mutable o necesidad de herencia, usa una clase normal.
GO TO FULL VERSION