Gleichheit und annähernde Gleichheit von Objekten
Lassen Sie uns die equals
-Methode, die zum Vergleichen von Objekten verwendet wird, noch einmal durchgehen und uns mit der hashCode
-Methode vertraut machen, die für annähernde Vergleiche genutzt wird.
Methode zum Testen auf Gleichheit - "equals"
equals
-Methode überprüft standardmäßig, ob das als Parameter übergebene Objekt dieselbe Referenz hat wie das Objekt, mit dem es verglichen wird. Mit anderen Worten: Die Standardverhaltensweise prüft, ob die beiden Objekte identisch sind. Wenn die Referenz dieselbe ist, gibt die Methode true
zurück, andernfalls false
.Dies lässt sich mit folgendem Beispiel veranschaulichen. Die Klasse Book
hat keine eigene Implementierung der equals
-Methode, sodass sie auf die von Java bereitgestellte Standardimplementierung zurückgreift.
Book bookObject = new Book("Book object", 2000, "...");
Book anotherBookObject = bookObject;
if (bookObject.equals(anotherBookObject)) {
System.out.println("The books are the same");
} else {
System.out.println("The books aren't the same");
}
// wir erstellen nun ein Objekt mit demselben Inhalt, das jedoch ein eigenes Objekt ist
anotherBookObject = new Book("Book object", 2000, "...");
if (bookObject.equals(anotherBookObject)) {
System.out.println("The books are the same");
} else {
System.out.println("The books aren't the same");
}
Die interne Struktur der Buchobjekte (d.h. die Werte ihrer Instanzvariablen) ist im obigen Beispiel dieselbe, aber nur der erste Vergleich gibt "The books are the same
" aus. Dies liegt daran, dass die Referenzen im ersten Fall dieselben sind, d.h. das Objekt wird mit sich selbst verglichen. Der zweite Vergleich betrifft zwei verschiedene Objekte, obwohl die Variablen dieselben Werte haben.
Bei Strings funktioniert equals
wie erwartet: Es erklärt zwei inhaltlich identische Strings als "gleich", selbst wenn es sich um zwei separate Objekte handelt. Die Klasse String
hat die Standardimplementierung von equals
durch ihre eigene Implementierung ersetzt.
Wenn Sie möchten, dass Ihre eigenen Klassen mit der equals
-Methode verglichen werden, muss diese Methode in der Klasse definiert werden. Die Methode akzeptiert eine Referenz vom Typ Object
als Parameter, der jedes Objekt sein kann. Der Vergleich beginnt mit der Überprüfung der Referenzen. Danach wird der Objekttyp des Parameters mit dem instanceof
-Operator überprüft – wenn der Objekttyp nicht dem Typ unserer Klasse entspricht, kann das Objekt nicht dasselbe sein. Anschließend wird eine Version des Objekts erstellt, die vom gleichen Typ wie unsere Klasse ist, und die Objektvariablen werden miteinander verglichen.
public boolean equals(Object comparedObject) {
// wenn die Variablen sich an derselben Stelle befinden, sind sie gleich
if (this == comparedObject) {
return true;
}
// wenn comparedObject nicht vom Typ Book ist, sind die Objekte nicht gleich
if (!(comparedObject instanceof Book)) {
return false;
}
// das Objekt wird in ein Book-Objekt umgewandelt
Book comparedBook = (Book) comparedObject;
// wenn die Instanzvariablen der Objekte gleich sind, sind die Objekte ebenfalls gleich
if (this.name.equals(comparedBook.name) &&
this.published == comparedBook.published &&
this.content.equals(comparedBook.content)) {
return true;
}
// andernfalls sind die Objekte nicht gleich
return false;
}
Die Book
-Klasse in ihrer Gesamtheit:
public class Book {
private String name;
private String content;
private int published;
public Book(String name, int published, String content) {
this.name = name;
this.published = published;
this.content = content;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getPublished() {
return this.published;
}
public void setPublished(int published) {
this.published = published;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
public String toString() {
return "Name: " + this.name + " (" + this.published + ")\n"
+ "Content: " + this.content;
}
@Override
public boolean equals(Object comparedObject) {
// wenn die Variablen sich an derselben Stelle befinden, sind sie gleich
if (this == comparedObject) {
return true;
}
// wenn comparedObject nicht vom Typ Book ist, sind die Objekte nicht gleich
if (!(comparedObject instanceof Book)) {
return false;
}
// das Objekt wird in ein Book-Objekt umgewandelt
Book comparedBook = (Book) comparedObject;
// wenn die Instanzvariablen der Objekte gleich sind, sind die Objekte ebenfalls gleich
if (this.name.equals(comparedBook.name) &&
this.published == comparedBook.published &&
this.content.equals(comparedBook.content)) {
return true;
}
// andernfalls sind die Objekte nicht gleich
return false;
}
}
Nun gibt der Vergleich der Bücher true
zurück, wenn die Instanzvariablen der Bücher gleich sind.
Book bookObject = new Book("Book Object", 2000, "...");
Book anotherBookObject = new Book("Book Object", 2000, "...");
if (bookObject.equals(anotherBookObject)) {
System.out.println("The books are the same");
} else {
System.out.println("The books aren't the same");
}
The books are the same
Die ArrayList verwendet auch die equals
-Methode in ihrer internen Implementierung. Wenn die equals
-Methode in unseren Objekten nicht definiert ist, funktioniert die contains
-Methode der ArrayList nicht richtig. Wenn Sie den unten stehenden Code mit zwei Book
-Klassen ausprobieren, eine mit der equals
-Methode und eine ohne, werden Sie den Unterschied sehen.
ArrayList<Book> books = new ArrayList<>();
Book bookObject = new Book("Book Object", 2000, "...");
books.add(bookObject);
if (books.contains(bookObject)) {
System.out.println("Book Object was found.");
}
bookObject = new Book("Book Object", 2000, "...");
if (!books.contains(bookObject)) {
System.out.println("Book Object was not found.");
}
Dieses Vertrauen auf Standardmethoden wie equals
ist tatsächlich der Grund, warum Java verlangt, dass Variablen, die zu ArrayList und HashMap hinzugefügt werden, von Referenztypen sind. Jede Referenztyp-Variable verfügt über Standardmethoden wie equals
, was bedeutet, dass Sie die interne Implementierung der ArrayList-Klasse nicht ändern müssen, wenn Sie Variablen verschiedener Typen hinzufügen. Primitive Variablen haben solche Standardmethoden nicht.
Loading...
Annähernde Vergleiche mit hashCode
Neben equals
kann auch die hashCode
-Methode für annähernde Vergleiche von Objekten verwendet werden. Die Methode erstellt aus dem Objekt einen "Hash-Code", d.h. eine Zahl, die ein wenig über den Inhalt des Objekts aussagt. Wenn zwei Objekte denselben Hash-Wert haben, können sie gleich sein. Wenn zwei Objekte jedoch unterschiedliche Hash-Werte haben, sind sie mit Sicherheit ungleich.
Hash-Codes werden beispielsweise in HashMaps verwendet. Das interne Verhalten einer HashMap basiert darauf, dass Schlüssel-Wert-Paare in einem Array von Listen basierend auf dem Hash-Wert des Schlüssels gespeichert werden. Jeder Array-Index verweist auf eine Liste. Der Hash-Wert identifiziert den Array-Index, und die Liste, die sich an diesem Array-Index befindet, wird durchsucht. Der mit dem Schlüssel verknüpfte Wert wird nur dann zurückgegeben, wenn in der Liste genau derselbe Wert gefunden wird (die Gleichheitsprüfung erfolgt mit der equals
-Methode). Auf diese Weise muss die Suche nur einen Bruchteil der in der HashMap gespeicherten Schlüssel berücksichtigen.
Bisher haben wir als Schlüssel in einer HashMap nur String
- und Integer
-Objekte verwendet, die praktischerweise bereits fertige hashCode
-Methoden implementiert haben.
Lassen Sie uns ein Beispiel erstellen, in dem dies nicht der Fall ist: Wir bleiben bei den Büchern und führen Buchaufzeichnungen über ausgeliehene Bücher. Wir implementieren die Buchführung mit einer HashMap. Der Schlüssel ist das Buch, und der dem Buch zugeordnete Wert ist eine Zeichenkette, die den Namen des Ausleihenden angibt:
HashMap<Book, String> borrowers = new HashMap<>();
Book bookObject = new Book("Book Object", 2000, "...");
borrowers.put(bookObject, "Pekka");
borrowers.put(new Book("Test Driven Development", 1999, "..."), "Arto");
System.out.println(borrowers.get(bookObject));
System.out.println(borrowers.get(new Book("Book Object", 2000, "...")));
System.out.println(borrowers.get(new Book("Test Driven Development", 1999, "...")));
Pekka null null
Wir finden den Ausleiher, wenn wir nach demselben Objekt suchen, das als Schlüssel an die put
-Methode der HashMap übergeben wurde. Wenn wir jedoch nach demselben Buch, aber mit einem anderen Objekt suchen, wird kein Ausleiher gefunden, und wir erhalten stattdessen die null-Referenz. Der Grund liegt in der Standardimplementierung der hashCode
-Methode in der Object
-Klasse. Die Standardimplementierung erstellt einen hashCode
-Wert basierend auf der Objektreferenz, was bedeutet, dass Bücher mit demselben Inhalt, die dennoch unterschiedliche Objekte sind, unterschiedliche Ergebnisse von der hashCode
-Methode erhalten. Daher wird das Objekt nicht an der richtigen Stelle gesucht.
Damit die HashMap so funktioniert, wie wir es möchten, nämlich den Ausleiher zurückgibt, wenn ein Objekt mit dem richtigen Inhalt (nicht unbedingt dasselbe Objekt wie der ursprüngliche Schlüssel) angegeben wird, muss die Klasse, zu der der Schlüssel gehört, zusätzlich zur equals
-Methode auch die hashCode
-Methode überschreiben. Die Methode muss so überschrieben werden, dass sie für alle Objekte mit demselben Inhalt dasselbe numerische Ergebnis liefert. Auch einige Objekte mit unterschiedlichem Inhalt können dasselbe Ergebnis von der hashCode
-Methode erhalten. Es ist jedoch im Hinblick auf die Leistung der HashMap entscheidend, dass Objekte mit unterschiedlichem Inhalt so selten wie möglich denselben Hash-Wert erhalten.
Wir haben zuvor String
-Objekte als Schlüssel in HashMaps verwendet, daher können wir daraus schließen, dass die String
-Klasse eine gut funktionierende hashCode
-Implementierung hat. Wir werden die Berechnungsverantwortung an das String
-Objekt delegieren:
public int hashCode() {
return this.name.hashCode();
}
Die obige Lösung ist ziemlich gut. Wenn jedoch name
null ist, sehen wir einen NullPointerException
-Fehler. Lassen Sie uns dies beheben, indem wir eine Bedingung definieren: Wenn der Wert der Variable name
null ist, geben wir das Veröffentlichungsjahr als Hash-Wert zurück.
public int hashCode() {
if (this.name == null) {
return this.published;
}
return this.name.hashCode();
}
Nun werden alle Bücher mit demselben Namen in eine Gruppe zusammengefasst. Lassen Sie uns dies weiter verbessern, indem wir auch das Veröffentlichungsjahr in die Berechnung des Hash-Werts einbeziehen, die auf dem Buchtitel basiert.
public int hashCode() {
if (this.name == null) {
return this.published;
}
return this.published + this.name.hashCode();
}
Nun ist es möglich, das Buch als Schlüssel der HashMap zu verwenden. Damit wird das Problem, das wir zuvor hatten, gelöst, und die Buchausleiher werden gefunden:
HashMap<Book, String> borrowers = new HashMap<>();
Book bookObject = new Book("Book Object", 2000, "...");
borrowers.put(bookObject, "Pekka");
borrowers.put(new Book("Test Driven Development",1999, "..."), "Arto");
System.out.println(borrowers.get(bookObject));
System.out.println(borrowers.get(new Book("Book Object", 2000, "...")));
System.out.println(borrowers.get(new Book("Test Driven Development", 1999)));
Output:
Pekka Pekka Arto
Lassen Sie uns die Ideen noch einmal zusammenfassen: Damit eine Klasse als Schlüssel in einer HashMap verwendet werden kann, müssen Sie für sie definieren:
- die
equals
-Methode, sodass alle gleichen oder annähernd gleichen Objekte beim Vergleichtrue
zurückgeben und für alle anderenfalse
- die
hashCode
-Methode, sodass möglichst wenige Objekte denselben Hash-Wert erhalten