Apache Commons equals / hashCode builder [chiuso]


155

Sono curioso di sapere cosa pensano le persone qui di usare org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder per implementare equals/ hashCode? Sarebbe una pratica migliore che scrivere la tua? Gioca bene con Hibernate? Qual'è la tua opinione?


16
Non lasciarti tentare dalle funzioni reflectionEqualse reflectionHashcode; la performance è un killer assoluto.
Skaffman,

14
Ho visto alcune discussioni qui sulla parità ieri e ho avuto un po 'di tempo libero, quindi ho fatto un rapido test. Avevo 4 oggetti con differenti implementazioni uguali. eclipse generate, equalsbuilder.append, equalsbuilder.reflection e annotazioni pojomatic. La linea di base era l'eclissi. equalsbuilder.append ha impiegato 3,7x. pojomatic ha preso 5x. la riflessione basata ha richiesto 25,8 volte. È stato piuttosto scoraggiante perché mi piace la semplicità della riflessione basata e non sopporto il nome "pojomatic".
digitaljoel,

5
Un'altra opzione è Project Lombok; utilizza la generazione di bytecode piuttosto che la riflessione, quindi dovrebbe funzionare così come generato da Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Miglia

Risposte:


212

I costruttori di commons / lang sono fantastici e li uso da anni senza spese generali evidenti (con e senza ibernazione). Ma come scrive Alain, il modo di Guava è ancora più bello:

Ecco un esempio di fagiolo:

public class Bean{

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

}

Ecco equals () e hashCode () implementati con 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;
    }
}

e qui con Java 7 o versioni successive (ispirato a 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;
    }
}

Nota: questo codice originariamente faceva riferimento a Guava, ma come hanno sottolineato i commenti, da allora questa funzionalità è stata introdotta in JDK, quindi Guava non è più necessaria.

Come puoi vedere, la versione di Guava / JDK è più corta ed evita oggetti helper superflui. In caso di uguaglianza, consente anche di cortocircuitare la valutazione se una Object.equals()chiamata precedente restituisce false (per essere onesti: commons / lang ha un ObjectUtils.equals(obj1, obj2)metodo con semantica identica che potrebbe essere usato invece di EqualsBuilderconsentire il cortocircuito come sopra).

Quindi: sì, i costruttori di lang comuni sono molto preferibili rispetto ai metodi equals()e ai hashCode()metodi costruiti manualmente (o quei terribili mostri che Eclipse genererà per te), ma le versioni Java 7+ / Guava sono ancora migliori.

E una nota su Hibernate:

fai attenzione a usare raccolte pigre nelle tue implementazioni equals (), hashCode () e toString (). Ciò fallirà miseramente se non hai una sessione aperta.


Nota (about equals ()):

a) in entrambe le versioni di equals () sopra, potresti voler usare anche una o entrambe queste scorciatoie:

@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) a seconda della tua interpretazione del contratto equals (), puoi anche cambiare le righe

    if(obj instanceof Bean){

per

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

Se usi la seconda versione, probabilmente vorrai anche chiamare il super(equals())tuo equals()metodo. Le opinioni differiscono qui, l'argomento è discusso in questa domanda:

modo giusto per incorporare la superclasse in un'implementazione di Guava Objects.hashcode ()?

(anche se si tratta hashCode(), lo stesso vale per equals())


Nota (ispirata al commento di kayahr )

Objects.hashCode(..)(proprio come il sottostante Arrays.hashCode(...)) potrebbe funzionare male se hai molti campi primitivi. In tali casi, EqualsBuilderpotrebbe effettivamente essere la soluzione migliore.


34
Lo stesso sarà possibile con Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung,

3
Se lo sto leggendo correttamente, Josh Bloch dice in Effective Java , Item 8, che non dovresti usare getClass () nel tuo metodo equals (); piuttosto dovresti usare instanceof.
Jeff Olson,

6
@SeanPatrickFloyd Il modo Guava non solo crea un oggetto array per i vararg, ma converte TUTTI i parametri in oggetti. Quindi, quando gli passi 10 valori int, finirai con 10 oggetti Integer e un oggetto array. La soluzione commons-lang crea solo un singolo oggetto, indipendentemente da quanti valori aggiungi al codice hash. Lo stesso problema con equals. Guava converte tutti i valori in oggetti, commons-lang crea solo un singolo nuovo oggetto.
kayahr,

1
@wonhee Non sono assolutamente d'accordo sul fatto che sia meglio. L'uso di Reflection per calcolare i codici hash non è qualcosa che farei mai. L'overhead delle prestazioni è probabilmente trascurabile, ma sembra sbagliato.
Sean Patrick Floyd,

1
@kaushik rendendo una classe finale risolve effettivamente i potenziali problemi di entrambe le versioni (instanceof e getClass ()), a patto che implementiate il vostro uguale () solo nelle classi foglia
Sean Patrick Floyd,

18

Gente, svegliati! Dal momento che Java 7 ci sono metodi helper per uguale e hashCode nella libreria standard. Il loro utilizzo è del tutto equivalente all'utilizzo dei metodi Guava.


a) al momento della domanda, Java 7 non era ancora presente b) tecnicamente, non erano del tutto equivalenti. jdk ha il metodo Objects.equals rispetto ai metodi Objects.equal di Guava. Posso usare le importazioni statiche solo con la versione di Guava. Sono solo cosmetici, lo so, ma rendono il non Guava notevolmente più ingombrante.
Sean Patrick Floyd,

Questo non è un buon metodo per sovrascrivere un oggetto equivale al metodo a causa del fatto che Objects.equals chiamerà il metodo .equals dell'istanza. Se si chiama Objects.equals nel metodo .equals dell'istanza, si verificherà un overflow dello stack.
dardo,

Puoi fare un esempio, quando cade in un ciclo?
Mikhail Golubtsov,

OP richiede la sostituzione del metodo equals () all'interno di un oggetto. Come da documentazione del metodo statico Objects.equals (): "Restituisce true se gli argomenti sono uguali tra loro e false in caso contrario. Di conseguenza, se entrambi gli argomenti sono null, viene restituito true e se esattamente un argomento è null, false è restituito. In caso contrario, l'uguaglianza viene determinata utilizzando il metodo equals del primo argomento. "Pertanto, se si utilizzasse Objects.equals () all'interno dell'istanza sovrascritta equals () si chiamerebbe il proprio metodo equals, quindi Objects.equals () poi di nuovo se stesso, dando uno stack overflow.
dardo,

@dardo Stiamo parlando dell'implementazione dell'uguaglianza strutturale, quindi significa che due oggetti sono uguali tra loro se i loro campi lo fanno. Vedi l'esempio di Guava sopra, come viene implementato equals.
Mikhail Golubtsov,

8

Se non vuoi dipendere da una libreria di terze parti (forse stai eseguendo un dispositivo con risorse limitate) e non vuoi nemmeno digitare i tuoi metodi, puoi anche lasciare che l'IDE faccia il lavoro, ad esempio nell'uso di eclipse

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

Si otterrà il codice 'nativo', che si può configurare come ti piace e che si deve sostenere il cambiamento.


Esempio (eclissi Giunone):

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
Vero, ma il codice generato da Eclipse è illeggibile e non realizzabile.
Sean Patrick Floyd,

6
Per favore, non pensare mai a qualcosa di così terribile come l'eclipse generata equals. Se non vuoi dipendere da una libreria di terze parti, scrivi il metodo a una riga come Objects.equalte. Anche se usato solo una o due volte, rende il codice molto migliore!
maaartinus,

@maaartinus equals/ hashCodemetodi a una riga ???
FrVaBe,

1
@maaartinus Guava è una libreria di terze parti. Ho sottolineato che la mia soluzione può essere utilizzata se si desidera EVITARE di utilizzare librerie di terze parti.
FrVaBe,

1
@FrVaBe: E ho scritto "Se non vuoi dipendere da una libreria di terze parti, scrivi tu stesso il metodo a una riga come Objects.equal." E poi ho scritto il metodo a una riga che puoi usare per EVITARE usando Guava e ancora tagliare la lunghezza di uguale a circa la metà.
maaartinus,

6

EqualsBuilder e HashCodeBuilder hanno due aspetti principali che sono diversi dal codice scritto manualmente:

  • gestione nulla
  • creazione dell'istanza

EqualsBuilder e HashCodeBuilder semplificano il confronto di campi che potrebbero essere nulli. Con il codice scritto manualmente questo crea molta caldaia.

D'altra parte, EqualsBuilder creerà un'istanza per chiamata al metodo equals. Se i tuoi metodi uguali sono chiamati spesso questo creerà molte istanze.

Per Hibernate l'implementazione uguale e hashCode non fanno differenza. Sono solo un dettaglio di implementazione. Per quasi tutti gli oggetti di dominio caricati con ibernazione, l'overhead di runtime (anche senza analisi di escape) del Builder può essere ignorato . Il sovraccarico del database e delle comunicazioni sarà significativo.

Come menzionato da skaffman, la versione di riflessione non può essere utilizzata nel codice di produzione. La riflessione sarà rallentare e l '"implementazione" non sarà corretta per tutti tranne che per le classi più semplici. Anche tenere conto di tutti i membri è pericoloso poiché i membri appena introdotti cambiano il comportamento del metodo uguale. La versione di reflection può essere utile nel codice di test.


Non sono d'accordo sul fatto che l'implementazione della riflessione "non sarà corretta per tutti tranne che per le classi più semplici". Con i costruttori è possibile escludere esplicitamente i campi, se lo si desidera, quindi l'implementazione dipende davvero dalla definizione della chiave aziendale. Sfortunatamente, non posso essere in disaccordo con l'aspetto prestazionale dell'implementazione basata sulla riflessione.
digitaljoel,

1
@digitaljoel Sì, è possibile escludere i campi, ma queste definizioni non vengono salvate nel refactoring. Quindi non li ho menzionati apposta.
Thomas Jung,


0

Se hai solo a che fare con il bean di entità in cui id è una chiave primaria, puoi semplificare.

   @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

Secondo me non gioca bene con Hibernate, in particolare gli esempi della risposta che confronta lunghezza, nome e figli per qualche entità. Hibernate consiglia di utilizzare la chiave business da utilizzare in equals () e hashCode () e hanno i loro motivi. Se usi il generatore auto equals () e hashCode () sulla tua chiave aziendale, va bene, solo i problemi di prestazioni devono essere considerati come menzionato in precedenza. Ma la gente di solito usa tutte le proprietà di ciò che l'IMO è molto sbagliato. Ad esempio, attualmente sto lavorando a un progetto in cui le entità sono scritte usando Pojomatic con @AutoProperty, ciò che considero un modello davvero negativo.

I loro due scenari principali per usare hashCode () e equals () sono:

  • quando si inseriscono istanze di classi persistenti in un set (il modo consigliato per rappresentare associazioni a molti valori) e
  • quando si utilizza il ricollegamento di istanze distaccate

Quindi supponiamo che la nostra entità assomigli a questo:

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

Entrambi sono la stessa entità per Hibernate, che sono stati recuperati da qualche sessione ad un certo punto (il loro id e classe / tabella sono uguali). Ma quando implementiamo auto equals () un hashCode () su tutti gli oggetti di scena, che cosa abbiamo?

  1. Quando si inserisce entity2 nell'insieme persistente in cui l'entità1 esiste già, questa verrà inserita due volte e si tradurrà in un'eccezione durante il commit.
  2. Se si desidera collegare entità2 staccata alla sessione, dove entità1 esiste già (probabilmente non l'ho testato in particolare) non verranno unite correttamente.

Quindi, per il progetto al 99% che realizzo, utilizziamo la seguente implementazione di equals () e hashCode () scritta una volta nella classe di entità base, che è coerente con i concetti di 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();
}

Per l'entità transitoria faccio lo stesso di quello che Hibernate farà sul passo della persistenza, cioè. Uso la corrispondenza dell'istanza. Per gli oggetti persistenti confronto la chiave univoca, che è la tabella / id (non uso mai chiavi composite).


0

Nel caso, altri lo troveranno utile, ho escogitato questa classe Helper per il calcolo del codice hash che evita il sovraccarico di creazione di oggetti extra menzionato sopra (in effetti, il sovraccarico del metodo Objects.hash () è ancora più grande quando hai eredità in quanto creerà un nuovo array su ogni livello!).

Esempio di utilizzo:

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

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

L'helper 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);
    }
}

Ho immaginato che 10 sia il numero massimo ragionevole di proprietà in un modello di dominio, se ne hai di più dovresti pensare di refactoring e introdurre più classe invece di mantenere un mucchio di stringhe e primitive.

Gli svantaggi sono: non è utile se si hanno principalmente primitive e / o array di cui è necessario eseguire l'hash profondo. (Normalmente questo è il caso in cui devi affrontare oggetti piatti (di trasferimento) che sono fuori dal tuo controllo).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.