¿Qué problemas se deben tener en cuenta al anular equals y hashCode en Java?


617

¿Qué problemas / dificultades deben considerarse al anular equalsy hashCode?

Respuestas:


1439

La teoría (para los abogados de idiomas y los matemáticamente inclinados):

equals()( javadoc ) debe definir una relación de equivalencia (debe ser reflexiva , simétrica y transitiva ). Además, debe ser coherente (si los objetos no se modifican, debe seguir devolviendo el mismo valor). Además, o.equals(null)siempre debe devolver falso.

hashCode()( javadoc ) también debe ser coherente (si el objeto no se modifica en términos de equals(), debe seguir devolviendo el mismo valor).

La relación entre los dos métodos es:

Cada vez a.equals(b), entonces a.hashCode()debe ser igual que b.hashCode().

En la práctica:

Si anula uno, debe anular el otro.

Use el mismo conjunto de campos que usa para calcular equals()para calcular hashCode().

Utilice las excelentes clases auxiliares EqualsBuilder y HashCodeBuilder de la biblioteca Lang de Apache Commons . Un ejemplo:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

También recuerda:

Cuando utilice una colección o mapa basado en hash , como HashSet , LinkedHashSet , HashMap , Hashtable o WeakHashMap , asegúrese de que el hashCode () de los objetos clave que coloca en la colección nunca cambie mientras el objeto está en la colección. La forma a prueba de balas para garantizar esto es hacer que sus claves sean inmutables, lo que también tiene otros beneficios .


12
Punto adicional sobre appendSuper (): debe usarlo en hashCode () y equals () si y solo si desea heredar el comportamiento de igualdad de la superclase. Por ejemplo, si deriva directamente de Object, no tiene sentido porque todos los Objetos son distintos por defecto.
Antti Kissaniemi

312
Puede hacer que Eclipse genere los dos métodos por usted: Fuente> Generar hashCode () y equals ().
Rok Strniša

27
Lo mismo es cierto con Netbeans: developmentmentality.wordpress.com/2010/08/24/…
seinecle

66
@Darthenius Eclipse generado igual utiliza getClass () que puede causar problemas en algunos casos (vea el elemento efectivo de Java 8)
AndroidGecko

77
La primera comprobación nula no es necesaria dado el hecho de que instanceofdevuelve falso si su primer operando es nulo (Java efectivo nuevamente).
izaban

295

Hay algunos problemas que vale la pena tener en cuenta si se trata de clases que persisten utilizando un Mapeador de relaciones de objetos (ORM) como Hibernate, ¡si no creía que esto ya era excesivamente complicado!

Los objetos con carga lenta son subclases

Si sus objetos persisten usando un ORM, en muchos casos estará tratando con proxys dinámicos para evitar cargar objetos demasiado pronto desde el almacén de datos. Estos proxies se implementan como subclases de su propia clase. Esto significa que this.getClass() == o.getClass()volverá false. Por ejemplo:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

Si se trata de un ORM, usar o instanceof Persones lo único que se comportará correctamente.

Los objetos cargados perezosos tienen campos nulos

Los ORM generalmente usan los captadores para forzar la carga de objetos con carga lenta. Esto significa que person.nameserá nullsi personestá cargado de forma diferida, incluso si person.getName()fuerza la carga y devuelve "John Doe". En mi experiencia, esto surge más a menudo en hashCode()y equals().

Si se trata de un ORM, asegúrese de usar siempre getters y nunca coloque referencias en hashCode()y equals().

Guardar un objeto cambiará su estado

Los objetos persistentes a menudo usan un idcampo para mantener la clave del objeto. Este campo se actualizará automáticamente cuando se guarde un objeto por primera vez. No use un campo de identificación en hashCode(). Pero puedes usarlo equals().

Un patrón que uso a menudo es

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

Pero: no puedes incluir getId()en hashCode(). Si lo hace, cuando un objeto persiste, sus hashCodecambios. Si el objeto está en a HashSet, "nunca" lo encontrará de nuevo.

En mi Personejemplo, probablemente usaría getName()for hashCodey getId()plus getName()(solo para paranoia) para equals(). Está bien si hay algún riesgo de "colisiones" hashCode(), pero nunca está bien equals().

hashCode() debe usar el subconjunto de propiedades que no cambia equals()


2
@Johannes Brodwall: ¡no entiendo Saving an object will change it's state! hashCodedebe volver int, entonces, ¿cómo va a usar getName()? ¿Puede dar un ejemplo para suhashCode
jimmybondy

@jimmybondy: getName devolverá un objeto String que también tiene un código hash que se puede usar
mateusz.fiolka

85

Una aclaración sobre el obj.getClass() != getClass().

Esta declaración es el resultado de equals()ser una herencia hostil. Los JLS (especificación del lenguaje Java) especifica que si A.equals(B) == trueentonces B.equals(A)también hay que volver true. Si omite esa declaración, las clases heredadas que anulan equals()(y cambian su comportamiento) romperán esta especificación.

Considere el siguiente ejemplo de lo que sucede cuando se omite la declaración:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

Haciendo new A(1).equals(new A(1))también, el new B(1,1).equals(new B(1,1))resultado es verdadero, como debería ser.

Todo esto se ve muy bien, pero mira lo que sucede si intentamos usar ambas clases:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

Obviamente, esto está mal.

Si desea garantizar la condición simétrica. a = b si b = a y el principio de sustitución de Liskov llama super.equals(other)no solo en el caso de la Binstancia, sino que comprueba después, por Aejemplo:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

Lo que dará salida:

a.equals(b) == true;
b.equals(a) == true;

Donde, si ano es una referencia de B, entonces podría ser un ser un referente de clase A(porque extenderla), en este caso se llama super.equals() demasiado .


2
Puede hacer que los iguales sean simétricos de esta manera (si compara un objeto de superclase con un objeto de subclase, use siempre los iguales de la subclase) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (this);
pihentagy

55
@pihentagy: entonces obtendría un stackoverflow cuando la clase de implementación no anula el método equals. no es divertido.
Ran Biron

2
No obtendrá un stackoverflow. Si el método igual no se anula, volverá a llamar al mismo código, ¡pero la condición de recursión siempre será falsa!
Jacob Raihle

@pihentagy: ¿Cómo se comporta eso si hay dos clases derivadas diferentes? Si a ThingWithOptionSetApuede ser igual a a, Thingsiempre que todas las opciones adicionales tengan valores predeterminados, y del mismo modo para a ThingWithOptionSetB, entonces debería ser posible ThingWithOptionSetAcomparar a igual a a ThingWithOptionSetBsolo si todas las propiedades no básicas de ambos objetos coinciden con sus valores predeterminados, pero No veo cómo se prueba para eso.
supercat

77
El problema con esto es que rompe la transitividad. Si agrega B b2 = new B(1,99), entonces b.equals(a) == truey a.equals(b2) == truepero b.equals(b2) == false.
nickgrim

46

Para una implementación amigable con la herencia, consulte la solución de Tal Cohen, ¿Cómo implemento correctamente el método equals ()?

Resumen:

En su libro Guía efectiva del lenguaje de programación Java (Addison-Wesley, 2001), Joshua Bloch afirma que "simplemente no hay forma de extender una clase instanciable y agregar un aspecto mientras se preserva el contrato igual". Tal no está de acuerdo.

Su solución es implementar equals () llamando a otro no simétrico blindlyEquals () en ambos sentidos. blindlyEquals () se reemplaza por subclases, se hereda equals () y nunca se reemplaza.

Ejemplo:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

Tenga en cuenta que equals () debe funcionar en todas las jerarquías de herencia si se debe cumplir el Principio de sustitución de Liskov .


10
Eche un vistazo al método canEqual explicado aquí: el mismo principio hace que ambas soluciones funcionen, pero con canEqual no compara los mismos campos dos veces (arriba, px == this.x se probará en ambas direcciones): artima.com /lejava/articles/equality.html
Blaisorblade

2
En cualquier caso, no creo que sea una buena idea. Hace que el contrato de Equals sea innecesariamente confuso: alguien que toma dos parámetros Point, a y b, debe ser consciente de la posibilidad de que a.getX () == b.getX () y a.getY () == b.getY () puede ser verdadero, pero a.equals (b) y b.equals (a) son falsos (si solo uno es ColorPoint).
Kevin

Básicamente, esto es similar if (this.getClass() != o.getClass()) return false, pero flexible, ya que solo devuelve falso si las clases derivadas se molestan en modificar iguales. ¿Está bien?
Aleksandr Dubinsky

31

Todavía sorprendido de que ninguno recomendó la biblioteca de guayaba para esto.

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

23
java.util.Objects.hash () y java.util.Objects.equals () son parte de Java 7 (lanzado en 2011), por lo que no necesita Guava para esto.
herman

1
por supuesto, pero debe evitarlo ya que Oracle ya no proporciona actualizaciones públicas para Java 6 (este ha sido el caso desde febrero de 2013).
herman

66
Su thisen this.getDate()medios nada (que no sea el desorden)
Steve Kuo

1
Su expresión "no instanceof" necesita un soporte adicional: if (!(otherObject instanceof DateAndPattern)) {. De acuerdo con Hernan y Steve Kuo (aunque ese es un asunto de preferencia personal), pero de todos modos +1.
Amos M. Carpenter

26

Hay dos métodos en superclase como java.lang.Object. Necesitamos anularlos al objeto personalizado.

public boolean equals(Object obj)
public int hashCode()

Los objetos iguales deben producir el mismo código hash siempre que sean iguales, sin embargo, los objetos desiguales no necesitan producir códigos hash distintos.

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

Si desea obtener más, consulte este enlace como http://www.javaranch.com/journal/2002/10/equalhash.html

Este es otro ejemplo, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

¡Que te diviertas! @. @


Lo siento, pero no entiendo esta declaración sobre el método hashCode: no es legal si usa más variables que equals (). Pero si codifico con más variables, mi código se compila. ¿Por qué no es legal?
Adryr83

19

Hay un par de formas de verificar la igualdad de clase antes de verificar la igualdad de los miembros, y creo que ambas son útiles en las circunstancias correctas.

  1. Usa el instanceofoperador.
  2. Uso this.getClass().equals(that.getClass()).

Uso # 1 en una finalimplementación igual, o cuando implemento una interfaz que prescribe un algoritmo para iguales (como las java.utilinterfaces de colección, la forma correcta de verificar con (obj instanceof Set)o con cualquier interfaz que esté implementando). En general, es una mala elección cuando se pueden anular iguales porque eso rompe la propiedad de simetría.

La opción # 2 permite que la clase se extienda de manera segura sin anular iguales o romper la simetría.

Si su clase también lo es Comparable, los métodos equalsy también compareTodeben ser consistentes. Aquí hay una plantilla para el método igual en una Comparableclase:

final class MyClass implements Comparable<MyClass>
{

  

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

1
+1 por esto. Ni getClass () ni instanceof es una panacea, y esta es una buena explicación de cómo abordar ambos. No piense que hay alguna razón para no hacer esto.getClass () == that.getClass () en lugar de usar equals ().
Paul Cantrell

Hay un problema con esto. Las clases anónimas que no agregan ningún aspecto ni anulan el método equals fallarán la verificación getClass aunque deberían ser iguales.
steinybot

@Steiny No me queda claro que los objetos de diferentes tipos deberían ser iguales; Estoy pensando en diferentes implementaciones de una interfaz como una clase anónima común. ¿Puedes dar un ejemplo para apoyar tu premisa?
erickson

MyClass a = new MyClass (123); MyClass b = new MyClass (123) {// Anular algún método}; // a.equals (b) es falso cuando se usa this.getClass (). equals (that.getClass ())
steinybot

1
@Steiny Derecha. Como debería en la mayoría de los casos, especialmente si un método se anula en lugar de agregarse. Considere mi ejemplo anterior. Si no fuera así final, y el compareTo()método se anuló para invertir el orden de clasificación, las instancias de la subclase y la superclase no deberían considerarse iguales. Cuando estos objetos se usaron juntos en un árbol, las claves que eran "iguales" según una instanceofimplementación podrían no ser encontrables.
erickson


11

El método equals () se utiliza para determinar la igualdad de dos objetos.

ya que el valor int de 10 siempre es igual a 10. Pero este método equals () trata sobre la igualdad de dos objetos. Cuando decimos objeto, tendrá propiedades. Para decidir sobre la igualdad, se consideran esas propiedades. No es necesario que se tengan en cuenta todas las propiedades para determinar la igualdad y, con respecto a la definición de clase y el contexto, se puede decidir. Entonces el método equals () puede ser anulado.

siempre debemos anular el método hashCode () siempre que anulemos el método equals (). Si no, ¿qué pasará? Si usamos tablas hash en nuestra aplicación, no se comportará como se esperaba. Como el hashCode se usa para determinar la igualdad de los valores almacenados, no devolverá el valor correspondiente correcto para una clave.

La implementación predeterminada dada es el método hashCode () en la clase Object utiliza la dirección interna del objeto y lo convierte en entero y lo devuelve.

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

Ejemplo de salida de código:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966

7

Lógicamente tenemos:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

¡Pero no al revés!


6

Un problema que he encontrado es donde dos objetos contienen referencias entre sí (un ejemplo es una relación padre / hijo con un método de conveniencia en el padre para obtener todos los hijos).
Este tipo de cosas son bastante comunes cuando se hacen asignaciones de Hibernate, por ejemplo.

Si incluye ambos extremos de la relación en su código hash o pruebas iguales, es posible entrar en un bucle recursivo que termina en una excepción StackOverflowException.
La solución más simple es no incluir la colección getChildren en los métodos.


55
Creo que la teoría subyacente aquí es distinguir entre los atributos , agregados y asociaciones de un objeto. Las asociaciones no deberían participar equals(). Si un científico loco creara un duplicado de mí, seríamos equivalentes. Pero no tendríamos el mismo padre.
Raedwald
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.