Apache Commons equals / hashCode constructor [cerrado]

155

Tengo curiosidad por saber, ¿qué piensa la gente de usar org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder para implementar el equals/ hashCode? ¿Sería una mejor práctica que escribir la tuya? ¿Juega bien con Hibernate? ¿Cual es tu opinion?

aug70co
fuente
16
Simplemente no se sienta tentado por las funciones reflectionEqualsy reflectionHashcode; El rendimiento es un asesino absoluto.
skaffman
14
Ayer vi un poco de discusión sobre los iguales y tuve algo de tiempo libre, así que hice una prueba rápida. Tenía 4 objetos con diferentes implementaciones iguales. eclipse generado, equalsbuilder.append, equalsbuilder.reflection y anotaciones pojomáticas. La línea de base fue eclipse. equalsbuilder.append tomó 3.7x. pojomatic tomó 5x. La reflexión basada tomó 25.8x. Fue bastante desalentador porque me gusta la simplicidad de la reflexión basada y no puedo soportar el nombre de "pojomatic".
digitaljoel
55
Otra opción es el Proyecto Lombok; utiliza la generación de bytecode en lugar de la reflexión, por lo que debería funcionar tan bien como Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Millas

Respuestas:

212

Los creadores de commons / lang son geniales y los he estado usando durante años sin sobrecarga de rendimiento notable (con y sin hibernación). Pero como Alain escribe, la forma de la guayaba es aún mejor:

Aquí hay una muestra de Bean:

public class Bean{

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

}

Aquí está equals () y hashCode () implementado 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;
    }
}

y aquí con Java 7 o superior (inspirado en 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: este código originalmente hacía referencia a Guava, pero como los comentarios han señalado, esta funcionalidad se ha introducido en el JDK, por lo que Guava ya no es necesaria.

Como puede ver, la versión Guava / JDK es más corta y evita objetos auxiliares superfluos. En caso de igual, incluso permite un cortocircuito en la evaluación si una Object.equals()llamada anterior devuelve falso (para ser justos: commons / lang tiene un ObjectUtils.equals(obj1, obj2)método con semántica idéntica que podría usarse en lugar de EqualsBuilderpermitir un cortocircuito como se indicó anteriormente).

Entonces: sí, los constructores de lang comunes son muy preferibles a los métodos equals()y hashCode()métodos construidos manualmente (o esos monstruos horribles que Eclipse generará para usted), pero las versiones de Java 7+ / Guava son aún mejores.

Y una nota sobre Hibernate:

tenga cuidado con el uso de colecciones diferidas en sus implementaciones equals (), hashCode () y toString (). Eso fallará miserablemente si no tiene una sesión abierta.


Nota (sobre equals ()):

a) en ambas versiones de equals () anteriores, es posible que desee utilizar uno o ambos de estos atajos también:

@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) dependiendo de su interpretación del contrato equals (), también puede cambiar la (s) línea (s)

    if(obj instanceof Bean){

a

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

Si usa la segunda versión, probablemente también desee llamar super(equals())dentro de su equals()método. Las opiniones difieren aquí, el tema se discute en esta pregunta:

forma correcta de incorporar superclase en una implementación de Guava Objects.hashcode ()?

(aunque se trata hashCode(), lo mismo se aplica a equals())


Nota (inspirada en el comentario de kayahr )

Objects.hashCode(..)(al igual que el subyacente Arrays.hashCode(...)) podría funcionar mal si tiene muchos campos primitivos. En tales casos, en EqualsBuilderrealidad puede ser la mejor solución.

Sean Patrick Floyd
fuente
34
Lo mismo será posible con Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung
3
Si lo estoy leyendo correctamente, Josh Bloch dice en Effective Java , Item 8, que no debes usar getClass () en tu método equals (); más bien deberías usar instanceof.
Jeff Olson
66
@SeanPatrickFloyd The Guava-way no solo crea un objeto de matriz para los varargs, sino que también convierte TODOS los parámetros en objetos. Entonces, cuando le pase 10 valores int, terminará con 10 objetos Integer y un objeto de matriz. La solución commons-lang solo crea un único objeto, sin importar cuántos valores agregue al código hash. El mismo problema con equals. Guava convierte todos los valores en objetos, commons-lang solo crea un único objeto nuevo.
kayahr
1
@wonhee Estoy totalmente en desacuerdo con que esto es mejor. Usar Reflection para calcular códigos hash no es algo que haría. La sobrecarga de rendimiento es probablemente insignificante, pero simplemente se siente mal.
Sean Patrick Floyd
1
@kaushik hacer una final de clase realmente resuelve los problemas potenciales de ambas versiones (instanceof y getClass ()), siempre y cuando implemente su igual () en clases de hoja solamente
Sean Patrick Floyd
18

Amigos, despierten! Desde Java 7 hay métodos auxiliares para equals y hashCode en la biblioteca estándar. Su uso es totalmente equivalente al uso de los métodos de guayaba.

Mikhail Golubtsov
fuente
a) en el momento en que se hizo esta pregunta, Java 7 todavía no estaba allí b) técnicamente, no son del todo equivalentes. jdk tiene el método Objects.equals frente a los objetos Objects.equal de Guava. Puedo usar importaciones estáticas solo con la versión de Guava. Eso es solo cosméticos, lo sé, pero hace que la no guayaba sea notablemente más abarrotada.
Sean Patrick Floyd
Este no es un buen método para anular un método igual a objetos debido al hecho de que Objects.equals llamará al método .equals de la instancia. Si llama a Objects.equals dentro del método .equals de la instancia, provocará un desbordamiento de la pila.
dardo
¿Puedes dar un ejemplo, cuando cae en un bucle?
Mikhail Golubtsov
OP solicita anular el método equals () dentro de un Object. Según la documentación del método estático Objects.equals (): "Devuelve verdadero si los argumentos son iguales entre sí y falso de lo contrario. En consecuencia, si ambos argumentos son nulos, se devuelve verdadero y si exactamente un argumento es nulo, falso es devuelto. De lo contrario, la igualdad se determina usando el método equals del primer argumento. "Por lo tanto, si usó Objects.equals () dentro de la instancia anulada equals () llamaría a su propio método equals, luego Objects.equals () luego otra vez, dando un desbordamiento de pila.
dardo
@dardo Estamos hablando de implementar la igualdad estructural, por lo que significa que dos objetos son iguales entre sí si sus campos lo hacen. Vea el ejemplo de Guava arriba, cómo se implementa igual.
Mikhail Golubtsov
8

Si no desea depender de una biblioteca de terceros (tal vez está ejecutando un dispositivo con recursos limitados) e incluso no desea escribir sus propios métodos, también puede dejar que el IDE haga el trabajo, por ejemplo, en uso de eclipse

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

Obtendrá un código 'nativo' que puede configurar a su gusto y que debe admitir en los cambios.


Ejemplo (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
fuente
14
Es cierto, pero el código generado por Eclipse es ilegible e imposible de mantener.
Sean Patrick Floyd
66
Por favor, nunca pienses en algo tan terrible como el eclipse generado equals. Si no desea depender de una biblioteca de terceros, escriba el método de una línea como Objects.equalusted. ¡Incluso cuando se usa solo una o dos veces, mejora el código!
maaartinus
@maaartinus equals/ hashCodemétodos de una línea ???
Viernes
1
@maaartinus Guava es una biblioteca de terceros. Señalé que mi solución se puede utilizar si desea EVITAR el uso de bibliotecas de terceros.
Viernes
1
@FrVaBe: Y escribí "Si no quieres depender de una biblioteca de terceros, entonces escribe el método de una línea como Objects.equally". Y luego escribí el método de una línea que puedes usar para EVITAR usar Guava y aún así reducir la longitud de los iguales a aproximadamente la mitad.
maaartinus
6

EqualsBuilder y HashCodeBuilder tienen dos aspectos principales que son diferentes del código escrito manualmente:

  • manejo nulo
  • creación de instancias

EqualsBuilder y HashCodeBuilder facilitan la comparación de campos que podrían ser nulos. Con el código escrito manualmente, esto crea una gran cantidad de repeticiones.

El EqualsBuilder, por otro lado, creará una instancia por llamada al método igual. Si sus métodos de igualdad son llamados a menudo, esto creará muchas instancias.

Para Hibernate, la implementación de igual y hashCode no hace ninguna diferencia. Son solo un detalle de implementación. Para casi todos los objetos de dominio cargados con hibernación, se puede ignorar la sobrecarga de tiempo de ejecución (incluso sin análisis de escape) del generador . La sobrecarga de la base de datos y la comunicación será significativa.

Como skaffman mencionó, la versión de reflexión no se puede usar en el código de producción. La reflexión será lenta y la "implementación" no será correcta para todas las clases excepto las más simples. Tener en cuenta a todos los miembros también es peligroso ya que los miembros recién introducidos cambian el comportamiento del método igual. La versión de reflexión puede ser útil en el código de prueba.

Thomas Jung
fuente
No estoy de acuerdo con que la implementación de la reflexión "no sea correcta para todas las clases excepto las más simples". Con los constructores, puede excluir explícitamente los campos si lo desea, por lo que la implementación realmente depende de la definición de clave de su negocio. Desafortunadamente, no puedo estar en desacuerdo con el aspecto de rendimiento de la implementación basada en la reflexión.
digitaljoel
1
@digitaljoel Sí, puede excluir campos, pero estas definiciones no refactorizan guardar. Así que no los mencioné a propósito.
Thomas Jung
0

Si solo está tratando con el bean de entidad donde id es una clave principal, puede simplificar.

   @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
fuente
0

En mi opinión, no funciona bien con Hibernate, especialmente los ejemplos de la respuesta que comparan longitud, nombre e hijos para alguna entidad. Hibernate aconseja utilizar la clave comercial para ser utilizada en equals () y hashCode (), y tienen sus razones. Si usa el generador auto equals () y hashCode () en su clave comercial, está bien, solo los problemas de rendimiento deben considerarse como se mencionó anteriormente. Pero la gente generalmente usa todas las propiedades de lo que es IMO muy mal. Por ejemplo, actualmente estoy trabajando en un proyecto donde las entidades se escriben usando Pojomatic con @AutoProperty, lo que considero un patrón realmente malo.

Sus dos escenarios principales para usar hashCode () y equals () son:

  • cuando coloca instancias de clases persistentes en un Conjunto (la forma recomendada de representar asociaciones de muchos valores) y
  • cuando usa la reconexión de instancias separadas

Así que supongamos que nuestra entidad se ve así:

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

Ambos son la misma entidad para Hibernate, que se han obtenido de alguna sesión en algún momento (su id y clase / tabla son iguales). Pero cuando implementamos auto equals () a hashCode () en todos los accesorios, ¿qué tenemos?

  1. Cuando coloca la entidad2 en el conjunto persistente donde la entidad1 ya existe, esto se colocará dos veces y dará como resultado una excepción durante la confirmación.
  2. Si desea adjuntar la entidad2 separada a la sesión, donde la entidad1 ya existe, (probablemente, no lo he probado especialmente) no se fusionarán correctamente.

Entonces, para el 99% del proyecto que realizo, utilizamos la siguiente implementación de equals () y hashCode () escritos una vez en la clase de entidad base, que es consistente con los conceptos de 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();
}

Para la entidad transitoria, hago lo mismo que Hibernate hará en el paso de persistencia, es decir. Yo uso el partido de instancia. Para los objetos persistentes, comparo la clave única, que es la tabla / id (nunca uso claves compuestas).

Lukasz Frankowski
fuente
0

Por si acaso, a otros les resultará útil, se me ocurrió esta clase de ayuda para el cálculo de código hash que evita la sobrecarga de creación de objetos adicionales mencionada anteriormente (de hecho, la sobrecarga del método Objects.hash () es aún mayor cuando tienes herencia ya que creará una nueva matriz en cada nivel!).

Ejemplo de uso:

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

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

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

Me imaginé que 10 es el número máximo razonable de propiedades en un modelo de dominio, si tiene más, debería pensar en refactorizar e introducir más clases en lugar de mantener un montón de cadenas y primitivas.

Los inconvenientes son: no es útil si tiene principalmente primitivas y / o matrices que necesita analizar profundamente. (Normalmente este es el caso cuando tiene que lidiar con objetos planos (de transferencia) que están fuera de su control).

Vlad
fuente