1. Einführung
Wie vergleichen wir Objekte
In Java sind Objekte nicht einfach nur Daten – jedes hat seine eigene Adresse im Speicher. Der Operator == beantwortet die Frage „Ist das dieselbe Schachtel?“, vergleicht also Referenzen (Adressen) und nicht den Inhalt. Die Methode equals ist für den Vergleich des Inhalts gedacht. Standardmäßig verhält sich equals, falls es nicht überschrieben wird, wie ==.
Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);
System.out.println(p1 == p2); // false – das sind unterschiedliche Objekte im Speicher!
Damit zwei verschiedene Objekte als inhaltlich gleich gelten (z. B. alle relevanten Felder übereinstimmen), muss equals überschrieben werden. Und wenn Sie Objekte in Hash-Collections verwenden möchten, müssen Sie dazu passend auch hashCode korrekt überschreiben.
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // dasselbe Objekt
if (o == null || getClass() != o.getClass()) return false; // Klasse prüfen
Person person = (Person) o; // auf den passenden Typ casten
return age == person.age && name.equals(person.name); // Felder vergleichen
}
@Override
public int hashCode() {
return Objects.hash(name, age); // damit es mit HashSet/HashMap funktioniert
}
}
Eine solche Überschreibung ist für die korrekte Arbeit mit HashSet/HashMap entscheidend: Ohne equals und hashCode betrachten die Collections selbst inhaltlich gleiche Objekte als verschieden.
Die Äquivalenzrelation in equals hängt von Ihren Anforderungen ab: Man kann über alle Felder vergleichen oder nur über einen Teil der Felder (z. B. nur die E-Mail bei User) – wichtig sind Konsistenz und die Einhaltung des Vertrags.
Wo ist das besonders wichtig?
- In Collections auf Basis von Hash-Tabellen: HashSet, HashMap, LinkedHashSet und andere.
- Beim Suchen und Entfernen von Elementen in Collections: Ohne korrektes equals kann das gewünschte Objekt „nicht gefunden werden“.
- In der Business-Logik: Zwei User mit derselben E-Mail sollten als ein und derselbe Benutzer gelten.
hashCode – wozu wird er benötigt?
Hash-Collections (z. B. HashSet, HashMap) verwenden Hash-Tabellen. Die Methode hashCode berechnet eine ganze Zahl – die „Bucket-Adresse“, in die das Objekt einsortiert wird. Wenn zwei Objekte gemäß equals gleich sind, müssen ihre hashCode-Werte übereinstimmen. Wird diese Regel verletzt, verhalten sich die Collections unvorhersehbar.
2. Der equals- und hashCode-Vertrag
equals-Vertrag
Wesentliche Anforderungen an das Verhalten von equals:
- Reflexivität: a.equals(a) ist immer true.
- Symmetrie: wenn a.equals(b) true ist, dann ist auch b.equals(a) true.
- Transitivität: wenn a.equals(b) und b.equals(c), dann gilt auch a.equals(c) true.
- Konsistenz: solange sich die Objekte nicht ändern, bleibt das Ergebnis stabil.
- Vergleich mit null: Jedes Objekt ist ungleich null.
hashCode-Vertrag
- Wenn zwei Objekte gemäß equals gleich sind, sind ihre hashCode-Werte gleich.
- Wenn Objekte ungleich sind, dürfen ihre Hash-Codes übereinstimmen (Kollisionen sind zulässig, aber unerwünscht).
- Solange sich ein Objekt logisch nicht ändert, sollte sein hashCode konstant bleiben.
Mit anderen Worten: Übereinstimmender hashCode ist eine notwendige, aber keine hinreichende Bedingung für Gleichheit – gleicher Hash garantiert keine Gleichheit gemäß equals.
3. Implementierung von equals und hashCode: Beispiel
Betrachten wir die Klasse Person, bei der die Gleichheit über die Felder name und age definiert ist.
public class Person {
private String name;
private int age;
// Konstruktor, Getter, Setter ...
@Override
public boolean equals(Object o) {
if (this == o) return true; // Referenzen vergleichen
if (o == null || getClass() != o.getClass()) return false; // Klasse prüfen
Person person = (Person) o; // Typumwandlung
// Felder vergleichen
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 – eine verbreitete Wahl für eine Primzahl
return result;
}
}
- Zuerst schnelle Prüfungen: Referenz und Klasse.
- Dann der Vergleich der relevanten Felder.
- In hashCode wird die Primzahl 31 verwendet, um Kollisionen zu reduzieren.
Verwendung von Objects.equals und Objects.hash
Seit Java 7 vereinfacht die Klasse Objects den Code und macht ihn robuster im Umgang mit null:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals, hashCode und compareTo: wie sie zusammenhängen
Wie hängen equals und compareTo zusammen?
Das Interface Comparable definiert die Methode compareTo, die eine negative/null/positive Zahl für „kleiner/gleich/größer“ zurückgibt. Ideal ist, wenn aus a.compareTo(b) == 0 auch a.equals(b) folgt. Das Umgekehrte ist nicht zwingend.
Wenn die Konsistenz verletzt wird (z. B. vergleicht compareTo nur nach Alter, während equals nach Name und Alter vergleicht), können sich sortierende Collections wie TreeSet/TreeMap unerwartet verhalten: Objekte gelten in Bezug auf die Ordnung als „gleich“, aber inhaltlich als ungleich.
equals und hashCode in Collections
- In HashSet und HashMap basieren Einfügen/Suchen/Löschen auf einer korrekten Implementierung von equals und hashCode.
- Ohne Überschreibung betrachten diese Collections Objekte als „verschieden“, selbst wenn ihre Daten identisch sind.
5. Beispiele: wie das in Collections funktioniert
HashSet: Speicherung eindeutiger Objekte
Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplikat
System.out.println(people.size()); // 1, wenn equals/hashCode korrekt implementiert sind
Ohne korrektes equals/hashCode landen beide Objekte im Set.
HashMap: Suche nach Schlüssel
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
map.put(p1, "Benutzer 1");
System.out.println(map.get(p2)); // "Benutzer 1", wenn equals/hashCode korrekt implementiert sind
Ohne den Vertrag gibt die Collection null zurück – für sie sind das „verschiedene“ Schlüssel.
6. Best Practices: Tipps für die Implementierung
- Beziehen Sie in equals/hashCode alle Felder ein, die die „Identität“ des Objekts bestimmen.
- Verwenden Sie keine veränderlichen Felder (die sich nach dem Einfügen in Collections ändern) bei der Berechnung von hashCode.
- Überlassen Sie die Generierung der Methoden der IDE – das reduziert Tippfehler.
- Prüfen Sie in equals zuerst this == o, dann die Klasse, danach die Felder.
- Zum Vergleich von Objektfeldern verwenden Sie Objects.equals.
- Für den Hash-Code verwenden Sie Objects.hash oder das bewährte Muster mit dem Faktor 31.
7. Nützliche Details
Warum sollte man nicht nur hashCode verwenden?
Kollisionen sind unvermeidlich: Verschiedene Objekte können denselben hashCode haben. Der Hash ist nur ein schneller Hinweis auf den Bucket; die endgültige Entscheidung über Gleichheit trifft equals.
Kann man equals und hashCode nicht überschreiben?
Nur wenn Sie sicher sind, dass Objekte niemals inhaltlich verglichen werden und weder Schlüssel noch eindeutige Elemente in Collections sein werden. In der Praxis ist das selten.
Unterschied zwischen ==, equals und compareTo
| Operator/Methode | Was wird verglichen? | Wozu dient es? |
|---|---|---|
|
Referenzen (Speicheradressen) | Prüfung: „dasselbe Objekt?“ |
|
Inhalt der Objekte | Gleichheit gemäß Business-Logik |
|
Ordnung (kleiner/gleich/größer) | Sortierung, Anordnung |
8. Typische Fehler bei der Implementierung von equals und hashCode
Fehler Nr. 1: equals überschrieben, aber hashCode vergessen. Objekte gelten als gleich, landen aber in verschiedenen Buckets der Hash-Tabelle – Suchen und Entfernen funktionieren nicht mehr richtig.
Fehler Nr. 2: Veränderliche Felder in hashCode verwenden. Wenn sich ein Feld nach dem Einfügen in die Collection ändert, „geht das Objekt verloren“: Der Hash ändert sich, der Bucket aber nicht.
Fehler Nr. 3: Symmetrie/Transitivität von equals verletzt. a.equals(b) ergibt true, b.equals(a) jedoch false, oder die Transitivität bricht – Collections verhalten sich unvorhersehbar.
Fehler Nr. 4: In equals nicht auf die Klasse prüfen. Der Vergleich von Objekten unterschiedlicher Klassen führt zu falschen Ergebnissen oder Ausnahmen.
Fehler Nr. 5: Strings und Objekte mit == vergleichen. Der Operator == vergleicht Referenzen; verwenden Sie equals für den Inhalt.
Fehler Nr. 6: Keine Konsistenz zwischen compareTo und equals. Wenn a.compareTo(b) == 0, aber !a.equals(b), können TreeSet/TreeMap-Collections Elemente für die Ordnung als gleich, in Bezug auf Gleichheit jedoch als verschieden betrachten – eine Quelle für „Geisterelemente“ und Duplikate.
GO TO FULL VERSION