04.03.11 Besondere Methoden (equals, hashCode und toString)

Sie kennen bereits gewöhnliche Methoden von Klassen. Es gibt aber auch speziellere Methoden, die Sie in Ihre Klassen aufnehmen können. Dazu gehören equals, hashCode und toString. Doch was macht diese Methoden so speziell, dass es uns ein komplettes Kapitel wert ist? Dies werden Sie schon bald herausfinden!

Ursprung

Zuerst einmal gilt zu klären, woher diese Methoden überhaupt kommen, wo liegt ihr Ursprung und warum heißen sie genau so und nicht anders? Nun, jede Klasse, die nicht explizit von einer Klasse erbt (Hierzu lernen Sie im Kapitel über Vererbung mehr), stammt automatisch von der Klasse Object ab. Folglich erbt jede Klasse von Object oder einer Subklasse von Object und erhält deshalb auch alle Methoden von Object – u. a. equals, hashCode und toString. Sie können also davon ausgehen, dass jedem Objekt irgendeiner Klasse diese drei Methoden zur Verfügung stehen. Oftmals reicht es aber nicht aus, die Implementierung der Object-Klasse beizubehalten. Dann müssen diese Methoden auf Ihre eigene Art und Weise umgesetzt werden. Siehe hierzu auch das Kapitel über Polymorphie.

equals

Die equals-Methode kennen Sie bereits vom Vergleichen zweier Strings. In diesem Kapitel wurde Ihnen erklärt, warum Strings bzw. allgemein Objekte nicht über den == Operator auf inhaltliche Gleichheit überprüft werden können (es werden mit == lediglich die Referenzen geprüft), sondern warum dafür die equals-Methode verwendet werden muss. Wenn Sie also zwei Objekte vergleichen möchten, müssen Sie immer equals verwenden. Dies funktioniert tadellos für Objekte von Klassen, die diese Methode überschrieben haben – was Ihre eigene Klasse von Natur aus nicht tut. Wenn Ihre eigene Klasse auf inhaltliche Gleichheit überprüft werden soll, dann müssen Sie die equals-Methode in Ihre Klasse aufnehmen und ausprogrammieren. Hierdurch teilen Sie Java mit, welche Attribute miteinander verglichen werden müssen, um eine inhaltliche Gleichheit zu bestätigen.

Sehen wir uns zunächst einmal den Kopf dieser Methode an:

public boolean equals(Object obj)

Es handelt sich also um eine öffentliche Methode mit einem boolean als Rückgabetyp (stimmen die beiden Objekte überein? => true <= stimmen die Objekte nicht überein? => false <=) und einem Objekt als Parameter. Dieses Objekt der Klasse Object ist immer das Objekt, mit dem das Objekt, von welchem die equals-Methode aus aufgerufen wurde, verglichen werden soll.

Wird diese Methode nicht überschrieben, so wird der Standard-Code aus der Object-Klasse verwendet. Dieser sieht so aus:

public boolean equals(Object obj) {
  return (this == obj);
}

Sie sehen, dass der Standard-Code nicht gerade effektiv ist und nur true für Objekte mit der selben Referenz zurückliefert. Dieses Verhalten wollen wir nun ändern. Dazu erstellen wir eine neue Klasse Bottle, welche eine Flasche repräsentiert sowie einen Inhalt (Wasser, Cola, Bier, …) und eine Kapazität (0,33 Liter, 0,5 Liter, 1,0 Liter, …) besitzt:

public class Bottle {

  private float capacity = 0;
  private String content = null;
	
  public float getCapacity() {
    return this.capacity;
  }
	
  public void setCapacity(float capacity) {
    this.capacity = capacity;
  }
	
  public String getContent() {
    return this.content;
  }
	
  public void setContent(String content) {
    this.content = content;
  } 
}

Wenn Sie nun neue Objekte dieser Klasse anlegen und selbige mit identischen Daten füttern, sollten die Objekte den selben Inhalt besitzen. Dies überprüfen Sie mit equals.

public static void main(String[] args) {
		
  Bottle b1 = new Bottle();
  Bottle b2 = new Bottle();
  Bottle b3 = new Bottle();

  b1.setContent("Wasser");
  b1.setCapacity(1.0F);
  b2.setContent("Wasser");
  b2.setCapacity(1.0F);
  b3.setContent("Cola");
  b3.setCapacity(0.5F);
		
  if (b1.equals(b2)) {
    System.out.println("Flasche1 hat den selben Inhalt wie Flasche2");
  }
  else {
    System.out.println("Flasche1 hat NICHT den selben Inhalt wie Flasche2");
  }
  if (b2.equals(b3)) {
    System.out.println("Flasche2 hat den selben Inhalt wie Flasche3");
  }
  else {
    System.out.println("Flasche2 hat NICHT den selben Inhalt wie Flasche3");
  }
}

An der Konsolenausgabe sehen Sie aber, dass die beiden Flaschen (b1 und b2) mit identischem Inhalt von Java nicht als identisch angesehen werden.

Flasche1 hat NICHT den selben Inhalt wie Flasche2
Flasche2 hat NICHT den selben Inhalt wie Flasche3

Wie Sie sich sicher denken können, muss an dieser Stelle die equals-Methode überschrieben werden. Hierzu müssen Sie sich zuerst überlegen, was eine Flasche im Vergleich zu einer anderen Flasche inhaltlich identisch macht. Diese Punkte sind schnell gefunden:

  • Es muss sich die gleiche Flüssigkeit (content) in beiden Flaschen befinden
  • Es muss sich gleich viel Inhalt (capacity) in beiden Flaschen befinden

Mit diesem Wissen, können wir die equals-Methode in unserer Bottle-Klasse implementieren:

public boolean equals(Object obj) {
		
  // Vergleich von capacity
  // Vergleich von content
  return false;
}

Damit die Werte verglichen werden können, müssen wir das übergebene Objekt in unsere Bottle-Klasse casten.

Casting wurde das erste Mal in Kapitel 02.03. Primitive Datentypen angesprochen und wird Ihnen detailliert in Kapitel 04.05 Vererbung erklärt.

Anschließend sollte es für Sie mit Ihren jetzigen Kenntnissen eine Kleinigkeit sein, den Rest der Methode auszuprogrammieren:

public boolean equals(Object obj) {
		
  Bottle b = (Bottle)obj;
  if (this.capacity == b.getCapacity() && this.content.equals(b.getContent())) {
    return true;
  }
  return false;
}

Ihre Klasse sieht nun so aus:

public class Bottle {

  private float capacity = 0;
  private String content = null;
  
  public float getCapacity() {
    return this.capacity;
  }
  
  public void setCapacity(float capacity) {
    this.capacity = capacity;
  }
  
  public String getContent() {
    return this.content;
  }
  
  public void setContent(String content) {
    this.content = content;
  } 
  
  public boolean equals(Object obj) {
    
    Bottle b = (Bottle)obj;
    if (this.capacity == b.getCapacity() && this.content.equals(b.getContent())) {
      return true;
    }
    return false;
  }
}

Wenn Sie nun noch mal Ihre Main-Methode ausführen, können Sie die Veränderungen sehen:

Flasche1 hat den selben Inhalt wie Flasche2
Flasche2 hat NICHT den selben Inhalt wie Flasche3

Aber warum ist es jetzt so wichtig diese Methode equals zu nennen und Ihr ein Objekt zu übergeben? Nun, wenn Ihre Klasse von anderen Klassen verwendet wird, werden diese standardmäßig die equals-Methode mit einem Objekt der Klasse Object oder einer Kindklasse als Übergabeparameter verwenden - und nicht Ihre eigene Methode mit einem anderen Methodenkopf!

Da der equals-Methode ein Objekt einer beliebigen Klasse übergeben werden könnte, sollten Sie in der Praxis (bevor Sie casten) überprüfen, ob es sich auch tatsächlich um zwei identische Klassen handelt. Dies können Sie durch einen Vergleich des Rückgabewerts der Methode getClass (welche ebenfalls durch Object implementiert wird) erreichen. Zusätzlich sollten Sie überprüfen, ob nicht null, oder die Referenz auf das selbe Objekt übergeben wurde. Unsere neue equals-Methode würde dann so aussehen:

public boolean equals(Object obj) {
  
  if (obj == null) {
    return false;
  }
  if (obj == this) {
    return true;
  }
  if (obj.getClass() == this.getClass()) {
    Bottle b = (Bottle)obj;
    if (this.capacity == b.getCapacity() && this.content.equals(b.getContent())) {
      return true;
    }
  }
  return false;
}

Auf der nächsten Seite finden Sie Informationen zu hashCode, toString und den Verträgen für hashCode und equals

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.