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.
GO TO FULL VERSION