Apache Commons entspricht / hashCode Builder [geschlossen]

155

Ich bin gespannt, was die Leute hier über die Verwendung von org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder zur Implementierung von equals/ denken hashCode. Wäre es eine bessere Praxis als selbst zu schreiben? Spielt es gut mit Hibernate? Was ist deine Meinung?

aug70co
quelle
16
Lassen Sie sich einfach nicht von den reflectionEqualsund reflectionHashcodeFunktionen verführen. Die Leistung ist ein absoluter Killer.
Skaffman
14
Ich habe hier gestern eine Diskussion über Gleichberechtigte gesehen und hatte etwas Freizeit, also habe ich einen kurzen Test gemacht. Ich hatte 4 Objekte mit unterschiedlichen gleichen Implementierungen. Eclipse generiert, equalsbuilder.append, equalsbuilder.reflection und pojomatische Annotationen. Die Grundlinie war Sonnenfinsternis. equalsbuilder.append dauerte 3,7x. pojomatic dauerte 5x. Reflexionsbasiert dauerte 25,8x. Es war ziemlich entmutigend, weil ich die Einfachheit der Reflexion mag und den Namen "pojomatisch" nicht ausstehen kann.
Digitaljoel
5
Eine weitere Option ist Project Lombok. Es verwendet eher die Bytecode-Generierung als die Reflexion, daher sollte es genauso gut funktionieren wie die von Eclipse generierte. projectlombok.org/features/EqualsAndHashCode.html
Meilen

Antworten:

212

Die Commons / Lang-Builder sind großartig und ich benutze sie seit Jahren ohne merklichen Leistungsaufwand (mit und ohne Ruhezustand). Aber wie Alain schreibt, ist der Guavenweg noch schöner:

Hier ist ein Beispiel Bean:

public class Bean{

    private String name;
    private int length;
    private List<Bean> children;

}

Hier sind equals () und hashCode (), die mit Commons / Lang implementiert wurden:

@Override
public int hashCode(){
    return new HashCodeBuilder()
        .append(name)
        .append(length)
        .append(children)
        .toHashCode();
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return new EqualsBuilder()
            .append(name, other.name)
            .append(length, other.length)
            .append(children, other.children)
            .isEquals();
    } else{
        return false;
    }
}

und hier mit Java 7 oder höher (inspiriert von Guava):

@Override
public int hashCode(){
    return Objects.hash(name, length, children);
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return Objects.equals(name, other.name)
            && length == other.length // special handling for primitives
            && Objects.equals(children, other.children);
    } else{
        return false;
    }
}

Hinweis: Dieser Code bezog sich ursprünglich auf Guava. Wie in Kommentaren bereits erwähnt, wurde diese Funktionalität inzwischen im JDK eingeführt, sodass Guava nicht mehr benötigt wird.

Wie Sie sehen, ist die Guava / JDK-Version kürzer und vermeidet überflüssige Hilfsobjekte. Bei Gleichheit kann die Auswertung sogar kurzgeschlossen werden, wenn ein früherer Object.equals()Aufruf false zurückgibt (um fair zu sein: commons / lang hat eine ObjectUtils.equals(obj1, obj2)Methode mit identischer Semantik, die anstelle von verwendet werden könnteEqualsBuilder wie oben einen Kurzschluss zuzulassen).

Also: Ja, die Commons Lang Builder sind gegenüber manuell erstellten equals()und sehr vorzuziehenhashCode() Methoden Methoden (oder diesen schrecklichen Monstern, die Eclipse für Sie generiert) , aber die Java 7+ / Guava-Versionen sind noch besser.

Und ein Hinweis zu Hibernate:

Seien Sie vorsichtig bei der Verwendung von Lazy Collections in Ihren Implementierungen equals (), hashCode () und toString (). Das wird kläglich scheitern, wenn Sie keine offene Sitzung haben.


Hinweis (ungefähr gleich ()):

a) In beiden Versionen von equals () oben möchten Sie möglicherweise auch eine oder beide dieser Verknüpfungen verwenden:

@Override
public boolean equals(final Object obj){
    if(obj == this) return true;  // test for reference equality
    if(obj == null) return false; // test for null
    // continue as above

b) Abhängig von Ihrer Interpretation des equals () - Vertrags können Sie auch die Zeile (n) ändern.

    if(obj instanceof Bean){

zu

    // make sure you run a null check before this
    if(obj.getClass() == getClass()){ 

Wenn Sie die zweite Version verwenden, möchten Sie wahrscheinlich auch super(equals())Ihre equals()Methode aufrufen . Die Meinungen sind hier unterschiedlich, das Thema wird in dieser Frage diskutiert:

richtiger Weg, um Superklasse in eine Implementierung von Guava Objects.hashcode () zu integrieren?

(obwohl es darum geht hashCode(), gilt das gleiche für equals())


Hinweis (inspiriert von Kommentar von Kayahr )

Objects.hashCode(..)(genau wie der Basiswert Arrays.hashCode(...)) kann schlecht funktionieren, wenn Sie viele primitive Felder haben. In solchen Fällen EqualsBuilderkann tatsächlich die bessere Lösung sein.

Sean Patrick Floyd
quelle
34
Das gleiche wird mit Java 7 Objects.equals möglich sein: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung
3
Wenn ich es richtig lese, sagt Josh Bloch in Effective Java , Punkt 8, dass Sie getClass () in Ihrer equals () -Methode nicht verwenden sollten; Stattdessen sollten Sie instanceof verwenden.
Jeff Olson
6
@SeanPatrickFloyd Der Guava-Weg erstellt nicht nur ein Array-Objekt für die Varargs, sondern konvertiert auch ALLE Parameter in Objekte. Wenn Sie also 10 int-Werte übergeben, erhalten Sie 10 Integer-Objekte und ein Array-Objekt. Die Lösung commons-lang erstellt nur ein einzelnes Objekt, unabhängig davon, wie viele Werte Sie an den Hash-Code anhängen. Das gleiche Problem mit equals. Guava konvertiert alle Werte in Objekte, commons-lang erstellt nur ein einziges neues Objekt.
Kayahr
1
@wonhee Ich bin nicht der Meinung, dass dies besser ist. Die Verwendung von Reflection zur Berechnung von Hash-Codes würde ich niemals tun. Der Leistungsaufwand ist wahrscheinlich vernachlässigbar, fühlt sich aber einfach falsch an.
Sean Patrick Floyd
1
@kaushik ein Klassenfinale zu machen, löst tatsächlich die potenziellen Probleme beider Versionen (instanceof und getClass ()), solange Sie Ihre equals () nur in Blattklassen implementieren
Sean Patrick Floyd
18

Leute, wach auf! Seit Java 7 gibt es in der Standardbibliothek Hilfsmethoden für equals und hashCode . Ihre Verwendung entspricht voll und ganz der Verwendung von Guava-Methoden.

Mikhail Golubtsov
quelle
a) Zum Zeitpunkt der Beantwortung dieser Frage war Java 7 noch nicht vorhanden. b) Technisch gesehen sind sie nicht ganz gleichwertig. jdk hat die Objects.equals-Methode im Vergleich zu den Objects.equal-Methoden von Guava. Ich kann statische Importe nur mit Guavas Version verwenden. Das ist nur Kosmetik, ich weiß, aber es macht die Nicht-Guave merklich unübersichtlicher.
Sean Patrick Floyd
Dies ist keine gute Methode zum Überschreiben einer Methode für Objekte gleich, da Objects.equals die Methode .equals der Instanz aufruft. Wenn Sie Objects.equals innerhalb der .equals-Methode der Instanz aufrufen, führt dies zu einem Stapelüberlauf.
Dardo
Können Sie ein Beispiel geben, wenn es in eine Schleife fällt?
Mikhail Golubtsov
OP fordert dazu auf, die Methode equals () innerhalb eines Objekts zu überschreiben. Gemäß der Dokumentation der statischen Methode Objects.equals (): "Gibt true zurück, wenn die Argumente gleich sind, und andernfalls false. Wenn beide Argumente null sind, wird true zurückgegeben, und wenn genau ein Argument null ist, ist false zurückgegeben. Andernfalls wird unter Verwendung der Methode equals des ersten Arguments Gleichheit bestimmt. "Deshalb , wenn Sie verwendet Objects.equals () innerhalb der überschriebenen Instanz equals () es es, seine eigene Methode equals nennen würde, dann Objects.equals () dann wieder selbst, was einen Stapelüberlauf ergibt.
Dardo
@dardo Wir sprechen über die Implementierung struktureller Gleichheit. Das bedeutet, dass zwei Objekte einander gleich sind, wenn ihre Felder dies tun. Sehen Sie sich das Guava-Beispiel oben an, wie Equals implementiert wird.
Mikhail Golubtsov
8

Wenn Sie nicht von einer Bibliothek eines Drittanbieters abhängig sein möchten (möglicherweise führen Sie ein Gerät mit begrenzten Ressourcen aus) und sogar keine eigenen Methoden eingeben möchten, können Sie die IDE auch die Aufgabe ausführen lassen, z. B. bei der Verwendung von Eclipse

Source -> Generate hashCode() and equals()...

Sie werden ‚native‘ Code erhalten , die Sie können , wie Sie konfigurieren wie und welche Sie müssen auf Veränderungen unterstützen.


Beispiel (Eclipse Juno):

import java.util.Arrays;
import java.util.List;

public class FooBar {

    public String string;
    public List<String> stringList;
    public String[] stringArray;

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((string == null) ? 0 : string.hashCode());
        result = prime * result + Arrays.hashCode(stringArray);
        result = prime * result
                + ((stringList == null) ? 0 : stringList.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FooBar other = (FooBar) obj;
        if (string == null) {
            if (other.string != null)
                return false;
        } else if (!string.equals(other.string))
            return false;
        if (!Arrays.equals(stringArray, other.stringArray))
            return false;
        if (stringList == null) {
            if (other.stringList != null)
                return false;
        } else if (!stringList.equals(other.stringList))
            return false;
        return true;
    }

}
FrVaBe
quelle
14
Stimmt, aber der von Eclipse generierte Code ist nicht lesbar und nicht wartbar.
Sean Patrick Floyd
6
Bitte denken Sie niemals an etwas so Schreckliches wie die durch die Sonnenfinsternis erzeugte equals. Wenn Sie nicht von einer Bibliothek eines Drittanbieters abhängig sein möchten, schreiben Sie die einzeilige Methode wie Objects.equalSie. Selbst wenn es nur ein- oder zweimal verwendet wird, verbessert es den Code erheblich!
Maaartinus
@maaartinus equals/ hashCodeeinzeilige Methoden ???
FrVaBe
1
@maaartinus Guava ist eine Bibliothek von Drittanbietern. Ich habe darauf hingewiesen, dass meine Lösung verwendet werden kann, wenn Sie die Verwendung von Bibliotheken von Drittanbietern vermeiden möchten.
FrVaBe
1
@FrVaBe: Und ich schrieb: "Wenn Sie nicht von einer Bibliothek eines Drittanbieters abhängig sein möchten, schreiben Sie die einzeilige Methode wie Objects.equal selbst." Und dann habe ich die einzeilige Methode geschrieben, mit der Sie Guava VERMEIDEN und trotzdem die Länge von gleich auf etwa die Hälfte reduzieren können.
Maaartinus
6

EqualsBuilder und HashCodeBuilder haben zwei Hauptaspekte, die sich von manuell geschriebenem Code unterscheiden:

  • Nullbehandlung
  • Instanzerstellung

Der EqualsBuilder und der HashCodeBuilder erleichtern den Vergleich von Feldern, die null sein können. Mit manuell geschriebenem Code entsteht eine Menge Boilerplate.

Der EqualsBuilder erstellt dagegen eine Instanz pro equals-Methodenaufruf. Wenn Ihre Gleichheitsmethoden häufig aufgerufen werden, werden viele Instanzen erstellt.

Für den Ruhezustand machen die Implementierung von equals und hashCode keinen Unterschied. Sie sind nur ein Implementierungsdetail. Bei fast allen Domänenobjekten, die im Ruhezustand geladen sind, kann der Laufzeitaufwand (auch ohne Escape-Analyse) des Builders ignoriert werden . Der Datenbank- und Kommunikationsaufwand wird erheblich sein.

Wie Skaffman erwähnte, kann die Reflection-Version nicht im Produktionscode verwendet werden. Die Reflexion wird zu langsam sein und die "Implementierung" wird nicht für alle außer den einfachsten Klassen korrekt sein. Die Berücksichtigung aller Mitglieder ist ebenfalls gefährlich, da neu eingeführte Mitglieder das Verhalten der Methode "Gleich" ändern. Die Reflection-Version kann im Testcode hilfreich sein.

Thomas Jung
quelle
Ich bin nicht der Meinung, dass die Reflexionsimplementierung "nicht für alle außer den einfachsten Klassen korrekt sein wird". Mit den Buildern können Sie Felder explizit ausschließen, wenn Sie möchten, sodass die Implementierung wirklich von Ihrer Geschäftsschlüsseldefinition abhängt. Leider kann ich dem Leistungsaspekt der reflexionsbasierten Implementierung nicht widersprechen.
Digitaljoel
1
@digitaljoel Ja, Sie können Felder ausschließen, aber diese Definitionen werden beim Speichern nicht umgestaltet. Also habe ich sie nicht absichtlich erwähnt.
Thomas Jung
0

Wenn Sie sich nur mit der Entity-Bean befassen, bei der id ein Primärschlüssel ist, können Sie dies vereinfachen.

   @Override
   public boolean equals(Object other)
   {
      if (this == other) { return true; }
      if ((other == null) || (other.getClass() != this.getClass())) { return false; }

      EntityBean castOther = (EntityBean) other;
      return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
   }
DEREK LEE
quelle
0

Meiner Meinung nach spielt es mit Hibernate nicht gut, insbesondere mit den Beispielen aus der Antwort, in denen Länge, Name und Kinder für eine Entität verglichen werden. Hibernate empfiehlt die Verwendung eines Geschäftsschlüssels für equals () und hashCode () zu verwenden, und sie haben ihre Gründe. Wenn Sie für Ihren Geschäftsschlüssel den Generator auto equals () und hashCode () verwenden, ist dies in Ordnung. Es müssen nur die Leistungsprobleme wie oben erwähnt berücksichtigt werden. Aber die Leute nutzen normalerweise alle Eigenschaften, was IMO sehr falsch ist. Zum Beispiel arbeite ich gerade an einem Projekt, in dem Entitäten mit Pojomatic mit @AutoProperty geschrieben werden, was ich für ein wirklich schlechtes Muster halte.

Die beiden Hauptszenarien für die Verwendung von hashCode () und equals () sind:

  • wenn Sie Instanzen persistenter Klassen in ein Set einfügen (die empfohlene Methode zur Darstellung von Assoziationen mit vielen Werten) und
  • Wenn Sie das erneute Anhängen von getrennten Instanzen verwenden

Nehmen wir also an, unsere Entität sieht folgendermaßen aus:

class Entity {
  protected Long id;
  protected String someProp;
  public Entity(Long id, String someProp);
}

Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");

Beide sind dieselbe Entität für den Ruhezustand, die zu einem bestimmten Zeitpunkt aus einer Sitzung abgerufen wurden (ID und Klasse / Tabelle sind gleich). Aber was haben wir, wenn wir auto equals () a hashCode () auf allen Requisiten implementieren?

  1. Wenn Sie die Entität2 in die persistente Gruppe einfügen, in der die Entität1 bereits vorhanden ist, wird dies zweimal gesetzt und führt während des Festschreibens zu einer Ausnahme.
  2. Wenn Sie die getrennte Entität2 an die Sitzung anhängen möchten, in der Entität1 bereits vorhanden ist, werden sie (wahrscheinlich habe ich dies nicht speziell getestet) nicht ordnungsgemäß zusammengeführt.

Für ein 99% -Projekt, das ich mache, verwenden wir die folgende Implementierung von equals () und hashCode (), die einmal in der Basisentitätsklasse geschrieben wurden, was mit den Hibernate-Konzepten übereinstimmt:

@Override
public boolean equals(Object obj) {
    if (StringUtils.isEmpty(id))
        return super.equals(obj);

    return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}

@Override
public int hashCode() {
    return StringUtils.isEmpty(id)
        ? super.hashCode()
        : String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}

Für die vorübergehende Entität mache ich dasselbe, was der Ruhezustand im Persistenzschritt tun wird, dh. Ich benutze die Instanzübereinstimmung. Für die persistenten Objekte vergleiche ich den eindeutigen Schlüssel, der die Tabelle / ID ist (ich verwende niemals zusammengesetzte Schlüssel).

Lukasz Frankowski
quelle
0

Nur für den Fall, dass andere es nützlich finden, habe ich mir diese Helper-Klasse für die Berechnung von Hash-Code ausgedacht, die den oben erwähnten zusätzlichen Aufwand für die Objekterstellung vermeidet (tatsächlich ist der Aufwand für die Objects.hash () -Methode sogar noch größer, wenn Sie dies getan haben Vererbung, da auf jeder Ebene ein neues Array erstellt wird!).

Anwendungsbeispiel:

public int hashCode() {
    return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}

public int hashCode() {
    return HashCode.hash(super.hashCode(), occupation, children);
}

Der HashCode-Helfer:

public class HashCode {

    public static int hash(Object o1, Object o2) {
        return add(Objects.hashCode(o1), o2);
    }

    public static int hash(Object o1, Object o2, Object o3) {
        return hash(Objects.hashCode(o1), o2, o3);
    }

    ...

    public static int hash(Object o1, Object o2, ..., Object o10) {
        return hash(Objects.hashCode(o1), o2, o3, ..., o10);
    }

    public static int hash(int initial, Object o1, Object o2) {
        return add(add(initial, o1), o2);
    }

    ...

    public static int hash(int initial, Object o1, Object o2, ... Object o10) {
        return add(... add(add(add(initial, o1), o2), o3) ..., o10);
    }

    public static int hash(long value) {
        return (int) (value ^ (value >>> 32));
    }

    public static int hash(int initial, long value) {
        return add(initial, hash(value));
    }

    private static int add(int accumulator, Object o) {
        return 31 * accumulator + Objects.hashCode(o);
    }
}

Ich habe herausgefunden, dass 10 die maximal vernünftige Anzahl von Eigenschaften in einem Domänenmodell ist. Wenn Sie mehr haben, sollten Sie darüber nachdenken, mehr Klassen umzugestalten und einzuführen, anstatt einen Haufen von Zeichenfolgen und Grundelementen beizubehalten.

Die Nachteile sind: Es ist nicht nützlich, wenn Sie hauptsächlich Grundelemente und / oder Arrays haben, die Sie tief hashen müssen. (Normalerweise ist dies der Fall, wenn Sie mit flachen (Übertragungs-) Objekten arbeiten müssen, die außerhalb Ihrer Kontrolle liegen.)

Vlad
quelle