CodeGym /Kurse /JAVA 25 SELF /equals, hashCode, toString: automatische Generierung

equals, hashCode, toString: automatische Generierung

JAVA 25 SELF
Level 22 , Lektion 2
Verfügbar

1. Automatische Generierung von equals, hashCode, toString

Wozu braucht man diese Methoden?

Wenn du mit Objekten in Java arbeitest, stößt du ziemlich schnell auf dieselben Aufgaben. Manchmal muss man prüfen, ob zwei Objekte gleich sind. Zum Beispiel herausfinden, ob es schon in einer Sammlung wie Set oder Map vorhanden ist. In anderen Fällen wird ein Objekt als Schlüssel in einer HashMap verwendet – ohne spezielle Vergleichsregeln geht das nicht. Und fast immer möchte man ein Objekt im Log oder auf dem Bildschirm so ausgeben, dass die Ausgabe nicht bloß Kauderwelsch wie MyClass@7b23ec81 ist, sondern etwas Sinnvolles.

Für diese Fälle hat jede Klasse in Java drei besondere Methoden:

  • equals(Object o) prüft die Gleichheit.
  • hashCode() gibt dem Objekt einen numerischen „Fingerabdruck“, den Collections wie Hashtabellen benötigen.
  • toString() liefert eine gut lesbare String-Darstellung des Objekts, was das Debuggen und Ausgeben stark vereinfacht.

Warum ist das in normalen Klassen mühsam?

In normalen Klassen müssen diese Methoden per Hand geschrieben werden. Und da beginnt die Langeweile und Kopfschmerzen. Es entsteht jede Menge Boilerplate, der die Klasse nur aufbläht. Man kann sich leicht vertun: ein Feld beim Vergleich vergessen, den hashCode falsch berechnen und dann rätselhafte Bugs jagen. Und wenn man der Klasse ein neues Feld hinzufügt, muss man wieder in all diese Methoden und alles anpassen.

Beispiel einer normalen Klasse


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 + "]";
    }
}

Kommt dir bekannt vor? Ja, und das nur für zwei Felder! Wie sieht das bei zwanzig aus?

Wie macht das ein Record

Eine Record-Klasse erledigt das für dich. Deklariere einfach:


public record Point(int x, int y) { }

Und Java generiert automatisch:

  • Konstruktor
  • Getter (x(), y())
  • equals, hashCode, toString

Automatisch generierte Methoden

  • equals vergleicht alle Komponenten des Records nach Wert.
  • hashCode wird über alle Komponenten berechnet.
  • toString liefert einen String der Form Point[x=1, y=2].

Schauen wir uns das live an!


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]
    }
}

Ausgabe:

true
true
Point[x=1, y=2]

Alles funktioniert wie erwartet – ganz ohne überflüssige Codezeilen!

2. Warum das wichtig ist: Collections, Debugging und Sicherheit

Korrektes Verhalten in Collections

Stell dir vor, du verwendest Objekte als Schlüssel in einer HashMap oder als Elemente in einem HashSet. Wenn equals und hashCode falsch implementiert sind, verhalten sich die Collections merkwürdig: Sie finden ein soeben hinzugefügtes Element nicht oder halten zwei verschiedene Objekte fälschlich für identisch.

Mit Record-Klassen kannst du sicher sein: Vergleich und Hash berücksichtigen immer alle Record-Komponenten (in der Reihenfolge, in der sie deklariert sind).

Beispiel: einen Record als Schlüssel verwenden


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!" — funktioniert!
    }
}

Beachte: p1 und p2 sind unterschiedliche Objekte (verschiedene Referenzen), enthalten aber dieselben Feldwerte und gelten daher als gleich. Mehr über Map und HashMap erfährst du in Level 26 :P

Komfort beim Debuggen und Loggen

Anstelle eines drögen Point@1a2b3c4d (wie es bei normalen Klassen standardmäßig passiert) wird ein Record schön und informativ ausgegeben:

Point[x=3, y=4]

Das spart beim Debuggen und Logging eine Menge Zeit.

3. Wie equals, hashCode, toString im Record funktionieren

Methode equals

Eine Record-Klasse implementiert equals so, dass zwei Objekte als gleich gelten, wenn:

  • Sie vom selben Typ sind (dieselbe Record-Klasse)
  • Alle ihre Komponenten gleich sind (== für Primitive, equals() für Objekte)

Vergleichsbeispiel


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

Methode hashCode

Der Hashcode wird über alle Record-Komponenten berechnet, üblicherweise mit der Standardmethode Objects.hash(...).


System.out.println(p1.hashCode()); // Zum Beispiel 994
System.out.println(p2.hashCode()); // Auch 994
System.out.println(p3.hashCode()); // Eine andere Zahl

Methode toString

Die String-Darstellung hat immer das Format:

ClassName[field1=value1, field2=value2, ...]

System.out.println(p1); // Point[x=1, y=2]

4. Überschreiben von equals, hashCode, toString: wann und wie?

Manchmal (selten, aber es kommt vor) muss man das Standardverhalten dieser Methoden ändern. Zum Beispiel möchtest du, dass toString einen anderen Formatstring zurückgibt oder der Vergleich nur über einen Teil der Felder erfolgt.

Achtung: Wenn du equals/hashCode überschreibst, tu das sehr bewusst! Eine Verletzung ihres „Contracts“ kann zu schwer auffindbaren Bugs führen.

Wie überschreibt man eine Methode

Deklariere einfach deine eigene Methode im Rumpf der Record-Klasse:


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)

Darf man equals/hashCode überschreiben?

Ja, aber es ist stark abzuraten, wenn du dir nicht absolut sicher bist, was du tust. Zum Beispiel wenn du willst, dass der Vergleich nur über das Feld x erfolgt (was schon fragwürdig ist):


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 (!)

Aber sei vorsichtig: Wenn du equals überschreibst, überschreibe immer auch hashCode – sonst funktionieren Collections nicht korrekt.

Best Practice

  • Wenn du nicht genau weißt, warum du überschreiben willst – lass es!
  • Für toString darfst du problemlos ein eigenes Format verwenden, wenn du möchtest.
  • Für equals/hashCode nur, wenn es einen triftigen Grund gibt und du die Konsequenzen verstehst.

5. Praxis: Objektvergleich und Records in Collections verwenden

Beispiel: Vergleich zweier Record-Objekte


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]
    }
}

Beispiel: einen Record als Schlüssel in einer HashMap verwenden

Stellen wir uns vor, wir haben eine Anwendung, in der wir die Anzahl der Besuche der Nutzer nach ihrem Namen und Alter speichern (man weiß ja nie, vielleicht gibt es im Club zwei „Ivan, 20 Jahre“).


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);

        // Prüfen wir, dass die Suche nach dem Wert korrekt funktioniert
        System.out.println(visits.get(new User("Ivan", 20))); // 5
        System.out.println(visits.get(new User("Ivan", 22))); // 2
    }
}

Wenn equals und hashCode nicht korrekt implementiert wären, würde die Suche nicht funktionieren. Mehr über Map und HashMap erfährst du in den Vorlesungen von Level 26 :P

6. Typische Fehler im Umgang mit equals, hashCode, toString in Record-Klassen

Fehler Nr. 1: Erwartung, dass man Felder nach der Erstellung ändern kann.
Record-Felder sind immer final, und verglichen wird nach den Werten, die im Konstruktor gesetzt wurden. Wenn du das interne Zustand „auf Trickart“ änderst (z. B. über ein veränderliches Objekt in einem Feld), können Vergleich und Hash inkonsistent werden.

Fehler Nr. 2: equals überschrieben, aber hashCode vergessen.
Wenn du eine dieser Methoden überschreibst – überschreibe immer auch die andere! Andernfalls verhalten sich Collections (HashSet, HashMap) unvorhersehbar.

Fehler Nr. 3: Erwartung, dass toString ein anderes Format hat.
Wenn du ein spezielles Format brauchst – überschreibe einfach toString. Standardmäßig ist das Format immer ClassName[field1=value1, field2=value2].

Fehler Nr. 4: Records für komplexe Klassen mit veränderlichen Feldern verwenden.
Record-Felder sollten unveränderlich sein. Wenn du als Feld z. B. eine ArrayList verwendest und jemand deren Inhalt ändert, können Vergleich und Hashcode „kaputtgehen“. Für Records sind unveränderliche Typen die bessere Wahl.

Fehler Nr. 5: Records für Klassen mit Verhalten verwenden, die keine Value-Objects sind.
Ein Record ist keine „kleine Kurzsyntax-Klasse“. Er ist ein Value-Object, gedacht zum Halten eines Wertevectors. Wenn du komplexe Logik, veränderlichen Zustand oder Vererbung brauchst, verwende eine normale Klasse.

1
Aufgabe
JAVA 25 SELF, Level 22, Lektion 2
Gesperrt
Buchdetails im Bibliotheksbericht 📚
Buchdetails im Bibliotheksbericht 📚
1
Aufgabe
JAVA 25 SELF, Level 22, Lektion 2
Gesperrt
Ortsidentifikation auf der Karte 📍
Ortsidentifikation auf der Karte 📍
1
Aufgabe
JAVA 25 SELF, Level 22, Lektion 2
Gesperrt
Schöne Darstellung des Benutzerprofils 🧑‍💻
Schöne Darstellung des Benutzerprofils 🧑‍💻
1
Aufgabe
JAVA 25 SELF, Level 22, Lektion 2
Gesperrt
Identifikation von Produkten anhand des Namens im Shop 🏷️
Identifikation von Produkten anhand des Namens im Shop 🏷️
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION