Styl użycia HashCodeBuilder i EqualsBuilder

Często używam Apache HashCodeBuilder i EqualsBuilder do równości obiektów przy użyciu reflection, ale ostatnio kolega powiedział mi, że używanie reflection może spowodować ogromny hit wydajności, jeśli encja zawiera wiele właściwości. Obawiając się, że mogę używać niewłaściwej implementacji, moje pytanie brzmi, które z poniższych podejść wolisz? I dlaczego?

public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Admin)) {
            return false;
        }
        Admin otherAdmin  = (Admin) o;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(getUserName(), otherAdmin.getUserName());
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getUserName());
        return builder.hashCode();
    }
}

Vs.

public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
      return EqualsBuilder.reflectionEquals(this, o, Arrays.asList(id));
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, Arrays.asList(id));
    }
}
Author: tintin, 2012-06-06

6 answers

Oczywiście druga opcja jest bardziej elegancka i prosta. Ale jeśli martwisz się o wydajność, powinieneś wybrać pierwsze podejście, druga metoda również zawodzi, jeśli uruchomiony jest menedżer zabezpieczeń.

Skorzystam z pierwszej opcji, jeśli będę w twojej sytuacji. Również jest błąd w pierwszym podejściu do generowania hashCode powinien to być Return builder.toHashCode(); zamiast return builder.hashCode (); (które zwraca hashcode Builder obiektów hashcode)

 15
Author: ssk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-23 14:50:24

Mimo że druga opcja jest bardziej atrakcyjna (bo to tylko jedna linijka kodu) wybrałbym pierwszą opcję.

Powodem jest po prostu wydajność. Po przeprowadzeniu małego testu znalazłem bardzo dużą różnicę czasu między nimi.

W celu pewnego wyobrażenia sobie czasu, stworzyłem dwie proste klasy:

package equalsbuildertest;

import java.math.BigDecimal;
import java.util.Date;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Class1 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class1(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class1() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

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

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

}

I:

package equalsbuildertest;

import java.math.BigDecimal;
import java.util.Date;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Class2 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class2(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class2() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Class2)) {
            return false;
        }
        Class2 other = (Class2) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(field1, other.field1);
        builder.append(field2, other.field2);
        builder.append(field3, other.field3);
        builder.append(field4, other.field4);
        builder.append(field5, other.field5);
        builder.append(field6, other.field6);
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getField1());
        builder.append(isField2());
        builder.append(getField3());
        builder.append(getField4());
        builder.append(getField5());
        builder.append(getField6());
        return builder.hashCode();

    };

}

Ta druga klasa jest prawie taka sama jak pierwsza, ale z różnymi równymi i hashCode.

Po to, stworzyłem następujące testy:

package equalsbuildertest;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import java.util.Date;

import org.junit.Test;

public class EqualsBuilderTest {

    @Test
    public void test1() {
        Class1 class1a = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class1 class1b = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class1a, class1b);
        }
    }

    @Test
    public void test2() {
        Class2 class2a = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class2 class2b = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class2a, class2b);
        }
    }

}

Testy są dość proste i służą jedynie do pomiaru czasu.

Wyniki były następujące:

  • test1 (2,024 s)
  • test2 (0,039 s)

Wybrałem ich, aby byli całkowicie równi, aby mieć najlepsze czasy. Jeśli zdecydujesz się zrobić test z warunkami NotEquals będziesz miał krótszy czas, ale zachowując bardzo dużą różnicę czasu zbyt.

Przeprowadzam te testy na 64-bitowy PROCESOR Intel Core i5-3317U @1.70 GHz x4 z Fedorą 21 i Eclipse Luna.

Podsumowując, nie zaryzykowałbym tak wielkiej różnicy w wydajności, aby zapisać kilka linijek kodu, których prawdopodobnie nie można i tak wpisać używając szablonu (w Eclipse pod Windows -> Preferences znajduje się w Javie -> Editor -> Templates), takiego jak:

${:import(org.apache.commons.lang3.builder.HashCodeBuilder, org.apache.commons.lang3.builder.EqualsBuilder)}
@Override
public int hashCode() {
    HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
    hashCodeBuilder.append(${field1:field});
    hashCodeBuilder.append(${field2:field});
    hashCodeBuilder.append(${field3:field});
    hashCodeBuilder.append(${field4:field});
    hashCodeBuilder.append(${field5:field});
    return hashCodeBuilder.toHashCode();
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    ${enclosing_type} rhs = (${enclosing_type}) obj;
    EqualsBuilder equalsBuilder = new EqualsBuilder();
    equalsBuilder.append(${field1}, rhs.${field1});
    equalsBuilder.append(${field2}, rhs.${field2});
    equalsBuilder.append(${field3}, rhs.${field3});
    equalsBuilder.append(${field4}, rhs.${field4});
    equalsBuilder.append(${field5}, rhs.${field5});${cursor}
    return equalsBuilder.isEquals();
}
 13
Author: sebadagostino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-07 20:03:42

Wolałbym drugą opcję z 2 powodów:

  1. Oczywiście łatwiej jest przeczytać

  2. Nie kupowałbym argumentów wydajności dla pierwszej opcji, chyba że zawierają one odpowiednią metrykę. Np. ile milisekund dodałoby "równe" oparte na odbiciu do typowego opóźnienia żądania od końca do końca? Ogólnie, jaki byłby procentowy wzrost? Nie wiedząc, że duże szanse są, że optymalizacja jest przedwczesna

 9
Author: Pavel Grushetzky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-26 18:02:15

Twoje pytanie, jak zostało napisane, wyraźnie ilustruje jedną z zalet drugiego podejścia:

W pierwszym przypadku, to jest bardzo łatwo popełnić błąd return builder.hashCode(), zamiast poprawnego return builder.toHashCode(), co spowoduje subtelne błędy, które mogą być bardzo trudne do wyśledzenia.

Drugi przypadek eliminuje możliwość wystąpienia tej literówki, co skutkuje mniejszym wbijaniem głowy w klawiaturę, próbując znaleźć błąd.

 3
Author: Krease,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-28 02:51:35

Powiedziałbym, że żadna z nich nie jest dobrą implementacją. Argumentowałbym, że EqualsBuilder nie jest dobrym frameworkiem do użycia z następujących powodów:

    Nie można wysuwać. Co jeśli jedno z pól, które próbujesz udowodnić równość, powinno traktować null i blank jako równe?
  1. Należy zachować listę zmiennych tak, jakby były to zmienne zakodowane na twardo. Oznacza to, że musisz wymienić wszystkie zmienne, które chcesz porównać. W tym momencie nie ma różnicy między A = = O. getA() && b = = o. getB() ...
  2. używanie reflection zajmuje dodatkowe zasoby, jak już zauważyłeś, w aplikacji korporacyjnej, która miażdży miliardy obiektów. Zrobienie tego odbicia równa się jest tak złe, jak wyciek pamięci.

Powiem, że musi być lepszy framework niż Apache.

 3
Author: Churk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-16 00:51:19

Zgadzam się z @ Churk, Apache HashCodeBuilder i EqualsBuilder nie są dobrze zaimplementowane. HashCodeBuilder nadal gra liczbami pierwszymi! Co więcej, wykonuje strasznie dużo niepotrzebnej pracy. Czytałeś źródło?

Od wersji Java 5 (jeśli nie wcześniej), AbstractHashMapnie używa modulo liczby pierwszej do zlokalizowania zasobnika skrótu. Zamiast tego Liczba łyżek jest potęgą dwóch, a do zlokalizowania łyżki używane są małe N bitów kodu skrótu.

Dalej, to będzie "mieszać" kod hash dostarczony przez aplikację tak, że bity są równomiernie rozłożone, a tym samym wiadra są równomiernie wypełnione.

Tak więc, prawidłowy sposób nadpisania obiektu int.hashCode() polega na zwróceniu najprostszej, stałej wartości o najwyższej arytmetyczności w populacji obiektów, które będą współistnieć w dowolnej kolekcji za pomocą klasy.

Zazwyczaj, wartość ID niezmodyfikowana jest najlepszym rozwiązaniem. Jeśli twoje pole ID jest całkowe, po prostu wrzuć je do (int) i zwróć to. Jeśli jest to ciąg znaków lub inny obiekt, po prostu zwróć jego kod skrótu. Rozumiesz. W przypadku identyfikatora złożonego zwróć pole (lub jego hashCode) z najbardziej odrębnymi wartościami. Mniej znaczy więcej.

Oczywiście, umowa pomiędzy hashCode () i equals () musi być spełniona. Tak więc equals() powinno być odpowiednio zaimplementowane. hashCode() nie musi używać pełnych kwalifikatorów potrzebnych do równości, ale wszystkie pola używane w hashCode () muszą być użyte w equals (). Tutaj metody takie jak StringUtils.równe (s1, S2) są przydatne do obsługi wartości null konsekwentnie i bezpiecznie.

 2
Author: Charlie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-16 15:10:26