Wer in Java eigene Klassen schreibt, kommt früher oder später an den Punkt, an dem equals(), hashCode() und toString() relevant werden. Diese drei Methoden stammen alle aus der Klasse Object, also der obersten Basisklasse in Java. Deshalb hat jede Klasse sie automatisch - aber oft reicht die Standard-Implementierung nicht aus. Besonders wenn du Objekte miteinander vergleichen oder in Collections wie HashSet oder HashMap speichern möchtest, wird es wichtig, diese Methoden richtig zu überschreiben.
Standardmäßig vergleicht equals() nur, ob zwei Variablen auf dasselbe Objekt im Speicher zeigen. Für einfache Werte wie Strings, Wrapper oder selbstgeschriebene Klassen ist das oft nicht das, was wir wollen. Wenn du z. B. zwei Person-Objekte hast, die zwar verschiedene Speicheradressen haben, aber denselben Namen und dasselbe Geburtsdatum, dann sollen sie inhaltlich als „gleich“ gelten.
public class Person {
private String name;
private int alter;
public Person(String name, int alter) {
this.name = name;
this.alter = alter;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // gleicher Speicherort
if (obj == null) return false; // null-Pruefung
if (getClass() != obj.getClass()) return false; // gleiche Klasse?
Person other = (Person) obj; // Typ-Cast
return name.equals(other.name) && alter == other.alter;
}
}
Damit haben wir erreicht, dass zwei Personen als gleich gelten, wenn Name und Alter übereinstimmen - unabhängig davon, ob sie an derselben Speicheradresse liegen oder nicht.
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
System.out.println(p1.equals(p2));
// Ausgabe:
true
Hier lauert eine der größten Fallen für Einsteiger: Immer wenn du equals() überschreibst, musst du auch hashCode() überschreiben. Das hat nichts mit Schönheit oder „Best Practice“ zu tun, sondern mit dem sogenannten Vertrag zwischen equals() und hashCode(). Der besagt:
Wenn zwei Objekte laut equals() gleich sind, müssen sie auch denselben Hashcode haben. Wenn sie unterschiedlich sind, dürfen sie denselben Hashcode haben - müssen aber nicht.
Warum das wichtig ist? Collections wie HashMap oder HashSet nutzen den Hashcode, um Objekte schnell zu finden oder zu prüfen, ob ein Element schon vorhanden ist. Wenn dein Hashcode nicht konsistent mit equals() ist, kann es passieren, dass ein Objekt doppelt in einem Set vorkommt - obwohl equals() sagt, dass es das gleiche ist. Das ist ein Albtraum beim Debuggen.
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + alter;
return result;
}
Hier kombinieren wir zwei Eigenschaften, die die Person eindeutig machen. Wir starten mit einer Zahl (17), multiplizieren mit einer Primzahl (31) und addieren die Hashcodes der einzelnen Felder. Das ist ein gängiges Muster, um Kollisionen zu vermeiden.
Wenn du Java 7 oder höher nutzt, kannst du es dir mit Objects.hash() noch einfacher machen:
@Override
public int hashCode() {
return java.util.Objects.hash(name, alter);
}
Schauen wir uns mal an, was passiert, wenn equals() und hashCode() nicht zueinander passen.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<Person> personen = new HashSet<>();
Person p1 = new Person("Lisa", 30);
Person p2 = new Person("Lisa", 30);
personen.add(p1);
personen.add(p2);
System.out.println("Anzahl im Set: " + personen.size());
}
}
Wenn hashCode() nicht korrekt implementiert ist, ergibt die Ausgabe:
// Ausgabe:
2
Obwohl die beiden Personen laut equals() gleich sind, hält das Set sie für unterschiedlich - weil der Hashcode verschieden ist. Sobald du hashCode() richtig überschreibst, bekommst du:
// Ausgabe:
1
Das zeigt, warum equals() und hashCode() immer zusammengehören - sie sind wie zwei Seiten derselben Medaille.
toString() ist nicht technisch zwingend nötig, aber extrem hilfreich. Wenn du z. B. in der Konsole ein Objekt ausgibst, bekommst du ohne eigene Implementierung nur so etwas wie:
Person@7ea987ac
Das hilft beim Debuggen nicht wirklich weiter. Also überschreiben wir toString() und geben sinnvolle Informationen aus:
@Override
public String toString() {
return "Person{name='" + name + "', alter=" + alter + "}";
}
Person p = new Person("Marco", 41);
System.out.println(p);
// Ausgabe:
Person{name='Marco', alter=41}
Schon viel verständlicher, oder? Eine gute toString()-Methode ist ein Segen beim Debuggen, Loggen und Testen.
Eine häufige Fehlerquelle ist, in equals() Felder zu vergleichen, die sich ändern können. Wenn du z. B. den Namen einer Person später änderst, verändert sich auch ihr Hashcode - und plötzlich ist das Objekt im HashSet nicht mehr auffindbar. Deshalb sollten Felder, die für Gleichheit und Hashcode herangezogen werden, möglichst unveränderlich sein.
Ein weiteres Problem: Vergiss niemals die Null-Prüfung! Wenn du einfach name.equals(other.name) aufrufst, ohne sicherzustellen, dass name nicht null ist, bekommst du schnell eine NullPointerException. Hier hilft dir die Methode Objects.equals(a, b):
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person other = (Person) obj;
return alter == other.alter && java.util.Objects.equals(name, other.name);
}
Das ist sicherer und übersichtlicher.
equals(), hashCode() und toString() sind kleine Methoden mit großer Wirkung. Wer sie versteht und richtig implementiert, verhindert viele schwer zu findende Fehler - besonders bei der Arbeit mit Hash-basierten Collections.
Wenn du eine Faustregel mitnimmst, dann diese:
Immer wenn du equals überschreibst, musst du auch hashCode überschreiben - und toString ist dein bester Freund beim Debuggen.
Damit hast du ein solides Fundament für sauberes, verständliches und zuverlässiges Java - und deine Kollegen werden es dir danken.

Hi, ich bin Marcel!
Als Fachinformatiker für Anwendungsentwicklung und IHK-geprüfter Ausbilder teile ich auf meinem Blog Grundlagen- und Fortgeschrittenen-Wissen für angehende Entwickler*innen und Interessierte, sowie weitere spannende Themen aus der IT.