CodeGym /Cursos /JAVA 25 SELF /equals, hashCode, toString: autogeneración

equals, hashCode, toString: autogeneración

JAVA 25 SELF
Nivel 22, Lección 2
Disponible

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.

1
Tarea
JAVA 25 SELF, nivel 22, lección 2
Bloqueada
Detalles del libro en el informe de la biblioteca 📚
Detalles del libro en el informe de la biblioteca 📚
1
Tarea
JAVA 25 SELF, nivel 22, lección 2
Bloqueada
Identificación de la ubicación en el mapa 📍
Identificación de la ubicación en el mapa 📍
1
Tarea
JAVA 25 SELF, nivel 22, lección 2
Bloqueada
Presentación atractiva del perfil de usuario 🧑‍💻
Presentación atractiva del perfil de usuario 🧑‍💻
1
Tarea
JAVA 25 SELF, nivel 22, lección 2
Bloqueada
Identificación de productos por nombre en la tienda 🏷️
Identificación de productos por nombre en la tienda 🏷️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION