Java - equals() und hashcode() überschreiben


Das leidige Thema equals() und hashcode()... Man muß glaube ich nicht erklären, wieso man die zwei Methoden überschreiben soll. Vielmehr ist es interessanter, wie man diese richtig überschreibt. Folgende Punkte müssen dabei berücksichtigt werden:

  • Welche Felder sind wichtig für den Vergleich auf Gleichheit?
  • Wie wichtig ist die Ausführungsgeschwindigkeit?
  • Wie sorge ich dafür, daß ich nicht vergesse equals() und hashcode() zu erweitern, wenn die Klasse geändert wurde (neue Felder kamen hinzu)?

Im Hinblick auf diese Punkte, betrachten wir diese drei Möglichkeiten equals() und hashcode() zu implementieren:

  • Reguläre Implementierung ohne Hilfsbibliotheken.
  • EqualsBuilder & HashcodeBuilder aus commons-lang3 per Builderklasse
  • EqualsBuilder & HashcodeBuilder aus commons-lang3 per Reflection

Die Implementierungen werden an folgender sehr einfachen (Kontainer- oder DTO-) Klasse getestet:

public class Person {
private String id;
private String name;
private Integer age;
private String comment;

[...] // getter, setter
}

Reguläre Implementierung

Normalerweise werden die equals() und hashcode()- Methoden nicht von Hand implementiert, sondern von einer IDE generiert, sodaß es zu solch einem Ergebnis kommt:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((age == null) ? 0 : age.hashCode());
    result = prime * result + ((comment == null) ? 0 : comment.hashCode());
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Person other = (Person) obj;
    if (age == null) {
        if (other.age != null)
            return false;
    } else if (!age.equals(other.age))
        return false;
    if (comment == null) {
        if (other.comment != null)
            return false;
    } else if (!comment.equals(other.comment))
        return false;
    if (id == null) {
        if (other.id != null)
            return false;
    } else if (!id.equals(other.id))
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

Der größte Vorteil dieser Lösung ist die Einfachheit - man muß über die Implementierung nicht viel nachdenken. Außerdem bieten die meisten IDEs eine entsprechende Generator-Funktion ohne zusätzlichen Plugins an.

Diese Lösung hat auch einige Nachteile:

  • Die Methoden müssen für jede Klasse extra generiert werden und das jedes Mal, wenn die Klasse geändert wird
  • Selbst in dieser einfachen Klasse benötigt die Implementierung ziemlich viel Codezeilen. Das macht die Klasse unleserlich und unübersichtlich.
  • Verschiedene IDEs implementieren die equals() und hashcode()-Methoden auf unterschiedliche Art und Weise ( instanceof statt getClass() ).

Builder-Pattern (Erbauer) mit Apache Lang

Weitere Möglichkeit die equals() und hashcode() zu implementieren, bietet sich mit Hilfe von HashCodeBuilder und EqualsBuilder aus der Bibliothek apache-lang3

@Override
public int hashCode() {
HashCodeBuilder b = new HashCodeBuilder();
b.append(this.id);
b.append(this.name);
b.append(this.age);
b.append(this.comment);
return b.toHashCode();
}

@Override
public boolean equals(Object obj) {

if (this == obj) {
return true;
}
if (!(obj instanceof PersonWithBuilder)) {
return false;
}

Person o = (Person)obj;
EqualsBuilder b = new EqualsBuilder();
b.append(this.id, o.id);
b.append(this.name, o.name);
b.append(this.age, o.age);
b.append(this.comment, o.comment);

return b.isEquals();
}

Der größte Vorteil dieser Lösung ist sind Klarheit und Übersichtlichkeit. Mit ein wenig Konfigurationsaufwand ist es auch hier möglich, die equals() und hashcode() mit IDE zu generieren.

Die Nachteile sind aber schwerwiegender:

  • Die Implementierung hängt nun von einer externen Bibliothek ab
  • Der Referenz- sowie Klassenvergleich müssen trotzdem implementiert werden
  • Schlechtere Performance

Vor allem ist die Ausführungsgeschwindigkeit der equals()-Methode, wenn auch minimal, schlechter als die reguläre Implementierung. Der EqualsBilder prüft bei jedem append auf die Gleichheit. Falls beispielsweise die Id's verschieden sind, werden die weiteren drei Anweisungen trotzdem ausgeführt, wobei der Builder keine Vergleiche mehr ausführt

[...]

public EqualsBuilder append(final int lhs, final int rhs) {
if (isEquals == false) {
return this;
}
isEquals = (lhs == rhs);
return this;
}

[...]

Reflection-Ansatz mit Apache Lang

Die dritte Implementierungsmöglichkeit beruht auf dem Reflection-Ansatz. Intern wird immer noch der EqualsBuilder verwendet, der mittels clazz.getDeclaredFields()  mit Daten "gefüttert" wird. Die Implementierung sieht dann wie folgt aus:

@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}

Der Code ist dadurch sehr einfach und übersichtlich, allerdings auf Kosten der Performance.

Falls aber die Ausführungsgeschwindigkeit keine so große Rolle spielt, hat diese Implementierung einen entscheidenden Vorteil - Stabilität. Auch wenn neue Felder hinzugefügt oder alte entfernt werden, müssen die equals() und hashcode() - Methoden nicht angepasst werden. Ach ja, es gibt eine Möglichkeit bestimmte Felder auszuschließen. Dazu müssen die Namen der betroffenen Felder wie folgt angegeben werden

@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "age", "comment");
}

@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj, "age", "comment");
}

Fazit

Jeder sollte selbst entscheiden, welche Implementierung für das aktuelle Projekt besser ist. Vielleicht macht es Sinn, am Anfang auf die Reflection-Methode zuzugreifen. Erst wenn das Datenmodel fertig und stabil ist, sorgt man für eine sinnvollere bzw. schnellere Implementierung. Hier ist noch der Performance-Vergleich der drei Methoden:

java hashcode performance

java equals performance

Den Quellcode zum Selbst-Ausprobieren gibt es auf github: example-hashcode-equals-test