Part 8

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"

Die 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");
}
Beispielausgabe
The books are the same 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");
}
Beispielausgabe

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.

Quiz

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, "...")));
Beispielausgabe

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:

Beispielausgabe

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 Vergleich true zurückgeben und für alle anderen false
  • die hashCode-Methode, sodass möglichst wenige Objekte denselben Hash-Wert erhalten
Loading
Loading
Loading
Sie haben das Ende dieses Abschnitts erreicht! Weiter zum nächsten Abschnitt: