04.03.11 Besondere Methoden (equals, hashCode und toString)

hashCode

Die Methode hashCode liefert einen möglichst eindeutigen (aber nicht zwingend einmaligen) Wert in Form eines ints zur Identifikation des Inhalts eines Objekts zurück – ist also der Methode equals ähnlich – erwartet aber keinen Übergabeparameter, da dieser Wert anhand der Eigenschaften des implementierenden Objekts berechnet wird. Wenn zwei Objekte einen unterschiedlichen hashCode besitzen, können Sie nicht inhaltlich gleich sein. Besitzen zwei Objekte einen identischen hashcode, können sie inhaltlich gleich sein, müssen aber nicht. Außerdem sollte die Berechnung des hashCodes schnell möglich sein.

Deshalb wird equals beim Vergleich von mehreren Objekten miteinander oftmals mit hashCode kombiniert. Zuerst wird „auf die Schnelle“ überprüft, ob beide Objekte den selben hashCode besitzen. Trifft dies zu, wird die langsamerere equals-Methode bemüht, um die wirkliche Gleichheit zu testen. Ist der hashCode allerdings nicht identisch, ist es völlig ausgeschlossen, dass die Objekte dennoch den selben Inhalt besitzen – es kann auf die umfangreiche Überprüfung mit equals verzichtet werden. Klassen im JDK, die dieses Verfahren einsetzen, sind z. B. java.util.HashSet, java.util.Hashtable, java.util.HashMap und deren Kindklassen. Im Kapitel über das Collection Framework lernen Sie mehr über diese Klassen.

Aus diesem Grund ist es umso wichtiger, dass Sie eine korrekte und schnelle Implementierung von hashCode wählen!

Beachten Sie, dass Sie sich – ähnlich wie bei equals – nicht auf die Standard-Implementierung verlassen können (welche im Falle von hashCode nativ realisiert wurde).

Aber wie schaut sie nun aus, die optimale Implementierung von hashCode? Ein Rezept dafür gibt es leider nicht, aber es gibt zumindest einige Richtlinien, an denen Sie sich halten können/müssen:

  • Die Berechnung sollte schnell sein
  • Alles, was von equals als identisch angesehen wird, hat auch den gleichen hashCode
  • Alles, was den selben hashCode hat, muss nicht laut equals identisch sein
  • In die Berechnung des hashCodes dürfen nur Attribute einfliesen, die auch beim Vergleich mit equals berücksichtigt wurden
  • Es müssen nicht alle Attribute, die von equals berücksichtigt wurden, auch im hashCode Verwendung finden. Attribute, die meistens den selben Wert haben (oder jedes einzelne Element eines großen Arrays) müssen z. B. oftmals nicht berücksichtigt werden
  • Jedes Attribut, das in die Berechnung mit einfliest, muss ein Integer-Wert zugewiesen werden können
  • Die Integer-Werte der Attribute werden addiert und optional mit einer beliebigen (Prim-)Zahl (nicht zu groß) multipliziert
  • Der mehrmalige Aufruf der hashCode-Methode muss immer den selben Wert zurückliefern – außer der Inhalt des Objekts hat sich seit dem letzten Aufruf verändert, oder der aktuelle Programmlauf wurde abgebrochen

Sie finden in der Auflistung oben den Hinweis, dass jedes Attribut einen Integer-Wert zugewiesen bekommen muss. Für primitive Datentypen wie byte, short, char und int sollte der Integer-Wert klar sein. Aber was passiert mit long, double, boolean, Objekten, …!? Hierzu existieren auch einige Richtlinien:

Typ Integer-Wert
byte, short, char, int Wird in int gecastet => (int)var
boolean Für true = 1, für false = 0 => (var ? 1 : 0)
long Wird in zwei Integer umgerechnet => (int)(var & 0xFFFFFFFF) und (int)(var >>> 32)
float Wird in sein Bit-Layout gewandelt, außer wenn der Wert = 0.0 oder -0.0 ist => ((var==0.0F) ? 0 : Float.floatToIntBits(var))
double Wird in sein Bit-Layout gewandelt, außer wenn der Wert = 0.0 oder -0.0 ist => ((var==0.0) ? 0L : Double.doubleToLongBits(var)) daraus entsteht ein long der noch mal (wie oben) behandelt werden muss
Objekte Aufruf der hashCode-Methode, sofern hashCode von dem Objekt implementiert wurde und das Objekt ungleich null ist => ((var==null) ? 0 : var.hashCode())

Der eigentliche Algorithmus sieht dann in etwa so aus:

public int hashCode() {

  int hashCode = 11; // willkürlicher Initialwert
  int multi = 29; // nicht zu große, zufällig gewählte Primzahl als Multiplikator
  hashCode += this.anInt;
  hashCode = hashCode * multi + (int)this.aChar;
  hashCode = hashCode * multi + (this.anObject == null ? 0 : this.anObject.hashCode());
  hashCode = hashCode * multi + (int)(this.aLong & 0xFFFFFFFF);
  hashCode = hashCode * multi + (int)(this.aLong >>> 32);
  ...
  return hashCode;
}

Sollte Ihre Klasse von einer anderen Klasse erben, die ebenfalls eine eigene Implementierung von hashCode besitzt, ist es meistens sinnvoll den hashCode der Elternklasse mit einzubeziehen (super.hashCode();).

Vertrag/contract für hashCode und equals

Wenn Sie eine eigene equals– und/oder hashCode-Methode in Ihrer Klasse ausprogrammieren, gibt es Tipps, wie Sie das am Besten angehen könnten (dieses Kapitel), aber auch Implementierungsregeln (Verträge/contracts), die Sie einhalten sollten, wozu Sie aber niemand zwingen kann. Diese Regeln finden Sie in der JavaDoc bei den entsprechenden Methoden der Klasse Object => equals und hashCode. Ins Deutsche übersetzt bedeuten sie sinngemäß:

  • equals
  • Wird ein Objekt mit sich selbst verglichen, wird true zurückgeliefert
  • Der Vergleich von Objekt 1 mit Objekt 2 muss das selbe Ergebnis liefern, wie der Vergleich von Objekt 2 mit Objekt 1
  • Wenn Objekt 1 gleich Objekt 2 ist und Objekt 2 gleich Objekt 3, dann muss Objekt 1 auch gleich Objekt 3 sein
  • Auch nach n Vergleichen zweier Objekte verändert sich das Resultat nicht
  • Kein Objekt entspricht null
  • hashCode
  • Egal wann hashCode aufgerufen wurde – der Rückgabewert ist immer gleich. Dies gilt aber nur, wenn
    1.) Sich der Inhalt des Objekts seit dem letzten Aufruf nicht verändert hat
    2.) Das Programm zwischenzeitlich nicht beendet wurde
  • Sind zwei Objekte gleich, generieren sie auch den selben hashCode
  • Sind zwei Objekte nicht gleich, müssen sie keinen unterschiedlichen hashCode haben – es wäre aber besser, wenn möglichst wenige bis gar keine Objekte den selben hashCode generieren

toString

Nachdem die letzten beiden Methoden etwas mit dem Vergleich von Objekten zu tun hatten, beschäftigt sich diese Methode mit etwas anderem – der Repräsentation eines Objekts als String.

Manchmal kommt es vor, dass Sie dem Benutzer (oder zur Überprüfung sich selbst) ein Objekt einer Klasse durch einen individuellen Text zeigen möchten – das menschliche Augen kann eben recht wenig mit Bytes, Hash-Codes oder einer equals-Methode anfangen. Hierzu fassen Sie die wichtigsten Informationen in der toString Methode Ihrer Klasse zusammen und geben sie in Form eines Strings zurück. Auch hier ist es wieder wichtig, dass Sie genau die toString-Methode verwenden, wie sie durch die Klasse Object vorgegeben wurde (

public String toString()

), da toString von anderen Klassen als Standardpräsentation des Objekts gesehen wird. Wenn Sie z. B. ein Objekt auf der Konsole ausgeben wollen, wird der Rückgabewert der toString-Methode ausgegeben.

Object obj = new Object();
String str = new String("Java-Blog-Buch.de ist toll!");
System.out.println(str);
System.out.println(str.toString());
System.out.println(obj);
System.out.println(obj.toString());

Natürlich müssen Sie die toString-Methode nicht überschreiben. Eine Klasse, die nie für einen Menschen lesbar dargestellt werden muss (wie z. B. eine Klasse zur Verschlüsselung), braucht auch keine toString-Methode. Sobald es aber relevant wird, eine Klasse für Menschen lesbar zu machen (z. B. für eine Klasse, die eine Person repräsentiert), sollten Sie toString überschreiben. Überschreiben Sie die toString-Methode nicht, rufen aber dennoch selbige auf, erhalten Sie eine etwas kryptische Ausgabe wie im Beispiel oben, als wir das Objekt obj ausgegeben haben => java.lang.Object@7f5a7f5a. Doch wie kommt diese Ausgabe zustande? Um das zu analysieren, sehen wir uns die Standardimplementierung der toString-Methode an:

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Es wird also der Name der Klasse, gefolgt von einem „@“ und dem Hash-Code des Objekts in hexadezimaler Schreibweise ausgegeben. Wie Sie toString sinnvoll überschreiben, hängt natürlich von der Klasse ab. Deshalb folgt jetzt ein kleines Beispiel in Form einer Klasse Person.

Zuerst programmieren wir die eigentliche Klasse. Sie erhält die Attribute firstname, lastname und age mitsamt den benötigten Getter- und Settermethoden.

public class Person {

  private String firstname = null;
  private String lastname = null;
  private int age = 0;
	
  public int getAge() {
    return this.age;
  }
	
  public void setAge(int age) {
    this.age = age;
  }

  public String getFirstname() {
    return this.firstname;
  }

  public void setFirstname(String firstname) {
    this.firstname = firstname;
  }

  public String getLastname() {
    return this.lastname;
  }

  public void setLastname(String lastname) {
    this.lastname = lastname;
  }
}

Als nächsten Schritt erstellen Sie sich eine weitere Klasse mit einer Main-Methode. Dort erzeugen Sie ein Array mit drei Personen, die Sie dann in einer Schleife ausgeben:

public static void main(String[] args) {
    
  Person[] persons = new Person[3];
  persons[0] = new Person();
  persons[1] = new Person();
  persons[2] = new Person();
    
  persons[0].setAge(21);
  persons[0].setFirstname("Andreas");
  persons[0].setLastname("Pries");
    
  persons[1].setAge(26);
  persons[1].setFirstname("Sebastian");
  persons[1].setLastname("Würkner");
    
  persons[2].setAge(20);
  persons[2].setFirstname("Stefan");
  persons[2].setLastname("Kiesel");
    
  for (int i = 0; i < persons.length; i++) {
    System.out.println(persons[i]);
  }
}

Natürlich erscheint auch diesmal die nichts sagende Ausgabe des Klassennamen in Kombination mit dem Hash-Code. Deshalb legen wir nun noch eine entsprechende toString-Methode in unserer Klasse an:

public String toString() {
  return this.lastname + ", " + this.firstname;
}

und testen das Programm erneut. Nun erscheint eine schönere Ausgabe auf der Konsole.

Pries, Andreas
Würkner, Sebastian
Kiesel, Stefan

Auf der nächsten Seite werden wir unser Beispiel zu toString noch etwas erweitern.

Previous Article
Next Article

9 Replies to “04.03.11 Besondere Methoden (equals, hashCode und toString)”

  1. m3

    Besserer Stil wäre:

    //den Kram drüber brauch man net - null wird durch instanceof geregelt
    
      if (obj instanceof bottle) {         //funktioniert jetzt auch für subklassen
        Bottle b = (Bottle)obj;
        if (this.capacity == b.getCapacity() && this.content.equals(b.getContent())) {
          return true;
        }
      }
      return false;
  2. Stefan Kiesel

    Hallo m3,

    danke für Ihren Kommentar! Da das Java Blog Buch ein Buch ist, sollten die Kapitel mehr oder weniger aufeinander aufbauen. Leider wird instanceof erst später im Kapitel 04.05 Vererbung angesprochen, weshalb ich mich gegen die Verwendung von instanceof entschieden habe. Aber selbstverständlich funktioniert es auch mit instanceof, da haben Sie natürlich Recht.

    Eine Anmerkung zu Ihrem Code/Kommentar im Code habe ich aber trotzdem:

    >> den Kram darüber braucht man nicht – null wird durch instanceof geregelt

    Dennoch sollte überprüft werden, ob es sich beim übergebenen Objekt um das selbe Objekt wie this handelt, da dies vom contract so verlangt wird.

    Gruß

    Stefan

  3. m3

    Hallo Stefan,

    Ich verstehe das ein Prüfung, ob es sich um das selbe Objekt handelt, sinnvoll sein kann – gerade wenn der Vergleichsalgorithmus zeitaufwendig ist (hier nicht der Fall).

    Ich verstehe jedoch nicht, was du mit „contract“ meinst.

    Gruß

    m3

  4. Stefan Kiesel

    Hallo m3,

    genau das ist der Grund, warum man überprüfen sollte, ob es sich um das identische Objekt handelt. Der „contract“ ist der Vertrag bzw. die Vorgaben von Sun, wie equals implementiert werden sollte. Dieser wird auf der zweiten Seite dieses Kapitels unter dem Punkt Vertrag/contract für hashCode und equals besprochen.

    Gruß

    Stefan

  5. Thomas Zenglein

    Sollten Attribute, die vom Typ float oder double sind nicht besser mit Float.compare bzw. Double.compare verglichen werden und abhängig vom Ergebnis entschieden werden, ob die betrachteten Attribute als gleich oder unterschiedlich gelten?

  6. Stefan Kiesel

    Hallo Thomas,

    wirft man einen Blick in den Quellcode von Float.compare stellt man fest, dass hier nicht die float-Werte sondern die Bit-Repräsentationen in Form von Integern verglichen werden. Sie haben also recht, dass Float.compare (und analog dazu auch Double.compare) etwas anderes als ein Vergleich mit == ist. In der Praxis fällt dies jedoch kaum ins Gewicht, weshalb ich den Artikel gerne unverändert, und Ihren Kommentar als zusätzlichen Hinweis da stehen lassen würde.

    Gruß
    Stefan

  7. Tobias

    Der Gebrauch von instanceof in equals()-Methoden (und dadurch die Miterfassung von Subklassen im Vergleich) ist schlecht, weil auch dadurch der Kontrakt der equals()-Methode verletzt wird. Siehe dazu „Effective Java, Second Edition“, Rezept 8.

    Der im Artikel gewählte Ansatz ist der Empfohlene.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.