Apache Commons equals / hashCode builder [zamknięty]


155

Ciekawi mnie, co ludzie tutaj myślą o używaniu org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder do implementacji equals/ hashCode? Czy byłaby to lepsza praktyka niż pisanie własnej? Czy dobrze współpracuje z Hibernate? Jakie jest Twoje zdanie?


16
Po prostu nie daj się skusić funkcjom reflectionEqualsi reflectionHashcode; występ jest absolutnym zabójcą.
skaffman

14
Widziałem wczoraj dyskusję na temat równych sobie i miałem trochę wolnego czasu, więc zrobiłem szybki test. Miałem 4 obiekty z różnymi implementacjami równości. wygenerowane zaćmienie, equalsbuilder.append, equalsbuilder.reflection i adnotacje pojomatic. Punktem odniesienia było zaćmienie. equalsbuilder.append zajęło 3,7x. pojomatic zajęło 5x. w oparciu o odbicie zajęło 25,8x. Było to dość zniechęcające, bo lubię prostotę refleksji opartej na pojęciu i nie mogę znieść nazwy „pojomatic”.
digitaljoel

5
Inną opcją jest Project Lombok; używa generowania kodu bajtowego zamiast odbicia, więc powinien działać równie dobrze jak wygenerowany przez Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Miles

Odpowiedzi:


212

Konstruktory Common / Lang są świetne i używam ich od lat bez zauważalnego narzutu wydajności (zi bez hibernacji). Ale jak pisze Alain, droga Guava jest jeszcze przyjemniejsza:

Oto przykładowy Bean:

public class Bean{

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

}

Oto equals () i hashCode () zaimplementowane za pomocą Commons / Lang:

@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;
    }
}

a tutaj z Javą 7 lub nowszą (inspirowaną Guavą):

@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;
    }
}

Uwaga: ten kod pierwotnie odwoływał się do guawy, ale jak zauważyły ​​komentarze, ta funkcjonalność została od tego czasu wprowadzona do JDK, więc guawa nie jest już wymagana.

Jak widać, wersja Guava / JDK jest krótsza i unika zbędnych obiektów pomocniczych. W przypadku równości pozwala nawet na zwarcie oceny, jeśli wcześniejsze Object.equals()wywołanie zwróci fałsz (żeby być sprawiedliwym: commons / lang ma ObjectUtils.equals(obj1, obj2)metodę o identycznej semantyce, której można użyć zamiast EqualsBuilderzezwalać na zwarcie jak powyżej).

A więc: tak, konstruktorzy języka wspólnego są bardziej preferowani niż ręcznie konstruowane equals()i hashCode()metody (lub te okropne potwory, które wygeneruje dla ciebie Eclipse), ale wersje Java 7+ / Guava są jeszcze lepsze.

I uwaga o Hibernate:

uważaj na używanie leniwych kolekcji w implementacjach equals (), hashCode () i toString (). To się nie powiedzie, jeśli nie masz otwartej sesji.


Uwaga (o równych ()):

a) w obu wersjach equals () powyżej możesz również chcieć użyć jednego lub obu tych skrótów:

@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) w zależności od Twojej interpretacji umowy equals (), możesz również zmienić linię (y)

    if(obj instanceof Bean){

do

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

Jeśli używasz drugiej wersji, prawdopodobnie chcesz również wywołać super(equals())wewnątrz swojej equals()metody. Zdania są tu różne, temat omawiany jest w tym pytaniu:

właściwy sposób na włączenie superklasy do implementacji Guava Objects.hashcode ()?

(choć o to chodzi hashCode(), to samo dotyczy equals())


Uwaga (inspirowana komentarzem od kayahr )

Objects.hashCode(..)(podobnie jak podstawa Arrays.hashCode(...)) może działać źle, jeśli masz wiele pól pierwotnych. W takich przypadkach EqualsBuildermoże faktycznie być lepszym rozwiązaniem.


34
To samo będzie możliwe z Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung,

3
Jeśli dobrze czytam, Josh Bloch mówi w Efektywnej Javie , pozycja 8, że nie powinieneś używać getClass () w swojej metodzie equals (); raczej powinieneś użyć instanceof.
Jeff Olson,

6
@SeanPatrickFloyd Guava-way nie tylko tworzy obiekt tablicowy dla varargs, ale także konwertuje WSZYSTKIE parametry na obiekty. Więc kiedy przekażesz do niego 10 wartości int, otrzymasz 10 obiektów Integer i obiekt tablicy. Rozwiązanie common-lang tworzy tylko jeden obiekt, niezależnie od tego, ile wartości dodasz do kodu skrótu. Ten sam problem z equals. Guawa konwertuje wszystkie wartości na obiekty, zwykły język tworzy tylko jeden nowy obiekt.
kayahr

1
@wonhee Zdecydowanie nie zgadzam się, że tak jest lepiej. Używanie Reflection do obliczania kodów skrótów nie jest czymś, co bym kiedykolwiek zrobił. Narzut wydajności jest prawdopodobnie znikomy, ale po prostu wydaje się zły.
Sean Patrick Floyd

1
@kaushik robi finał klasy w rzeczywistości rozwiązuje potencjalne problemy obu wersji (instanceof i getClass ()), o ile zaimplementujesz swoje equals () tylko w klasach liści
Sean Patrick Floyd

18

Ludzie, obudźcie się! Od Java 7 istnieją metody pomocnicze dla równań i hashCode w bibliotece standardowej. Ich użycie jest w pełni równoważne z użyciem metod guawy.


a) w czasie, gdy zadawano to pytanie, Java 7 jeszcze nie istniała b) technicznie nie są one do końca równoważne. jdk ma metodę Objects.equals w porównaniu z metodami Objects.equal Guavy. Mogę używać importu statycznego tylko z wersją Guavy. Wiem, że to tylko kosmetyki, ale to sprawia, że ​​produkty inne niż guawa są zauważalnie bardziej zagracone.
Sean Patrick Floyd

Nie jest to dobra metoda przesłonięcia metody objects.equals z powodu faktu, że Objects.equals wywoła metodę .equals instancji. Jeśli wywołasz Objects.equals w ramach metody .equals instancji, spowoduje to przepełnienie stosu.
dardo

Czy możesz podać przykład, kiedy wpada w pętlę?
Mikhail Golubtsov

OP prosi o przesłonięcie metody equals () w obiekcie. Zgodnie z dokumentacją metody statycznej Objects.equals (): "Zwraca wartość true, jeśli argumenty są sobie równe, a false w przeciwnym razie. W konsekwencji, jeśli oba argumenty mają wartość null, zwracana jest wartość true, a jeśli dokładnie jeden argument ma wartość null, to false is zwracane. w przeciwnym razie równości jest określana przy użyciu metody równa pierwszy argument. "Dlatego, jeśli użyto Objects.equals () wewnątrz przesłoniętych przykład równych () by to nazwać to metoda własne równe, wówczas Objects.equals () potem znowu siebie, powodując przepełnienie stosu.
dardo

@dardo Mówimy o implementacji równości strukturalnej, więc oznacza to, że dwa obiekty są sobie równe, jeśli ich pola tak. Zobacz powyższy przykład Guava, w jaki sposób equals jest implementowany.
Michaił Golubtsov

8

Jeśli nie chcesz polegać na bibliotece innej firmy (może korzystasz z urządzenia z ograniczonymi zasobami), a nawet nie chcesz wpisywać własnych metod, możesz również pozwolić IDE wykonać zadanie, np. Przy użyciu zaćmienia

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

Dostaniesz „native” kod którą można skonfigurować jak chcesz, a które muszą obsługiwać na zmiany.


Przykład (zaćmienie 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;
    }

}

14
To prawda, ale kod wygenerowany przez Eclipse jest nieczytelny i nie do utrzymania.
Sean Patrick Floyd

6
Proszę, nigdy, przenigdy nie myśl o czymś tak strasznym jak zaćmienie equals. Jeśli nie chcesz polegać na bibliotece innej firmy, napisz metodę Objects.equaljednowierszową, tak jak ty. Nawet jeśli jest używany tylko raz lub dwa razy, kod jest o wiele lepszy!
maaartinus

@maaartinus equals/ hashCodemetody jednowierszowe ???
FrVaBe

1
@maaartinus Guava to biblioteka innej firmy. Zwróciłem uwagę, że moje rozwiązanie może być używane, jeśli chcesz UNIKAĆ korzystania z bibliotek zewnętrznych.
FrVaBe

1
@FrVaBe: A ja napisałem „Jeśli nie chcesz polegać na bibliotece innej firmy, napisz metodę jednowierszową, taką jak Objects.equal siebie”. A potem napisałem metodę jednowierszową, której możesz użyć, aby UNIKAĆ używania guawy i nadal przyciąć długość równą do około połowy.
maaartinus

6

EqualsBuilder i HashCodeBuilder mają dwa główne aspekty, które różnią się od kodu napisanego ręcznie:

  • zerowa obsługa
  • tworzenie instancji

EqualsBuilder i HashCodeBuilder ułatwiają porównywanie pól, które mogą mieć wartość null. Z ręcznie napisanym kodem tworzy to wiele schematów.

Z drugiej strony EqualsBuilder utworzy wystąpienie dla wywołania metody equals. Jeśli metody equals są często wywoływane, spowoduje to powstanie wielu instancji.

W przypadku hibernacji implementacja equals i hashCode nie ma znaczenia. To tylko szczegół implementacji. W przypadku prawie wszystkich obiektów domeny załadowanych w trybie hibernacji, narzut czasu wykonywania (nawet bez analizy ucieczki) Konstruktora można zignorować . Koszty bazy danych i komunikacji będą znaczące.

Jak wspomniał skaffman, wersja refleksji nie może być używana w kodzie produkcyjnym. Refleksja będzie zbyt wolna, a „implementacja” nie będzie poprawna dla wszystkich klas poza najprostszymi. Uwzględnianie wszystkich członków jest również niebezpieczne, ponieważ nowo wprowadzani członkowie zmieniają zachowanie metody equals. Wersja odbicia może być przydatna w kodzie testowym.


Nie zgadzam się, że implementacja refleksji „nie będzie poprawna dla wszystkich klas oprócz najprostszych”. Dzięki konstruktorom możesz jawnie wykluczyć pola, jeśli chcesz, więc implementacja naprawdę zależy od definicji klucza biznesowego. Niestety nie mogę nie zgodzić się z aspektem wydajnościowym implementacji opartej na refleksji.
digitaljoel

1
@digitaljoel Tak, możesz wykluczyć pola, ale te definicje nie obejmują refaktoryzacji składowania. Więc celowo o nich nie wspomniałem.
Thomas Jung,


0

Jeśli masz do czynienia tylko z komponentem bean encji, w którym identyfikator jest kluczem podstawowym, możesz uprościć.

   @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();
   }

0

Moim zdaniem nie współgra to dobrze z Hibernate, zwłaszcza przykłady z odpowiedzi porównujące długość, imię i dzieci dla jakiejś jednostki. Hibernate zaleca użycie klucza biznesowego używanego w equals () i hashCode () i mają swoje powody. Jeśli używasz generatora auto equals () i hashCode () w swoim kluczu biznesowym, wszystko jest w porządku, wystarczy rozważyć problemy z wydajnością, jak wspomniano wcześniej. Ale ludzie zwykle używają wszystkich właściwości, co jest bardzo złe w IMO. Na przykład obecnie pracuję nad projektem, w którym encje są pisane przy użyciu Pojomatic z @AutoProperty, co uważam za naprawdę zły wzorzec.

Ich dwa główne scenariusze użycia hashCode () i equals () to:

  • po umieszczeniu wystąpień klas trwałych w zestawie (zalecany sposób reprezentowania skojarzeń o wielu wartościach) i
  • gdy używasz ponownego dołączania odłączonych instancji

Załóżmy więc, że nasz byt wygląda tak:

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");

Obie są tą samą jednostką dla Hibernate, które zostały w pewnym momencie pobrane z pewnej sesji (ich identyfikator i klasa / tabela są równe). Ale kiedy zaimplementujemy auto equals () a hashCode () na wszystkich właściwościach, co otrzymamy?

  1. Po umieszczeniu jednostki entity2 w zestawie trwałym, w którym jednostka1 już istnieje, zostanie to umieszczone dwukrotnie i spowoduje wyjątek podczas zatwierdzania.
  2. Jeśli chcesz dołączyć odłączoną jednostkę 2 do sesji, gdzie jednostka 1 już istnieje, (prawdopodobnie tego specjalnie nie testowałem) nie zostaną prawidłowo scalone.

Tak więc dla 99% projektu, który wykonuję, używamy następującej implementacji equals () i hashCode () zapisanych raz w podstawowej klasie encji, co jest zgodne z koncepcjami Hibernate:

@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();
}

Dla bytu przejściowego robię to samo, co Hibernate zrobi na kroku trwałości, tj. Używam dopasowania instancji. W przypadku obiektów trwałych porównuję klucz unikalny, którym jest table / id (nigdy nie używam kluczy złożonych).


0

Na wszelki wypadek, inni uznają to za przydatne, wymyśliłem tę klasę Helper do obliczania kodu skrótu, która pozwala uniknąć dodatkowego narzutu tworzenia obiektów wspomnianego powyżej (w rzeczywistości narzut metody Objects.hash () jest jeszcze większy, gdy masz dziedziczenie, ponieważ utworzy nową tablicę na każdym poziomie!).

Przykład użycia:

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

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

Pomocnik HashCode:

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);
    }
}

Doszedłem do wniosku, że 10 to maksymalna rozsądna liczba właściwości w modelu domeny, jeśli masz więcej, powinieneś pomyśleć o refaktoryzacji i wprowadzeniu większej liczby klas zamiast utrzymywania stosu ciągów znaków i prymitywów.

Wady są następujące: nie jest to przydatne, jeśli masz głównie prymitywy i / lub tablice, które musisz głęboko haszować. (Zwykle dzieje się tak, gdy masz do czynienia z płaskimi (przenoszonymi) przedmiotami, które są poza twoją kontrolą).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.