Quais problemas devem ser considerados ao substituir iguais e hashCode em Java?

617

Que questões / armadilhas devem ser consideradas ao substituir equalse hashCode?

Matt Sheppard
fonte

Respostas:

1439

A teoria (para os advogados de idiomas e os matematicamente inclinados):

equals()( javadoc ) deve definir uma relação de equivalência (deve ser reflexiva , simétrica e transitiva ). Além disso, ele deve ser consistente (se os objetos não forem modificados, será necessário continuar retornando o mesmo valor). Além disso, o.equals(null)deve sempre retornar falso.

hashCode()( javadoc ) também deve ser consistente (se o objeto não for modificado em termos de equals(), ele deve continuar retornando o mesmo valor).

A relação entre os dois métodos é:

Sempre a.equals(b), então a.hashCode()deve ser o mesmo que b.hashCode().

Na prática:

Se você substituir um, deverá substituir o outro.

Use o mesmo conjunto de campos que você usa equals()para calcular hashCode().

Use as excelentes classes auxiliares EqualsBuilder e HashCodeBuilder da biblioteca Apache Commons Lang . Um exemplo:

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

Lembre-se também:

Ao usar uma coleção ou mapa baseado em hash , como HashSet , LinkedHashSet , HashMap , Hashtable ou WeakHashMap , verifique se o hashCode () dos principais objetos que você coloca na coleção nunca muda enquanto o objeto está na coleção. A maneira à prova de balas para garantir isso é tornar suas chaves imutáveis, o que também tem outros benefícios .

Antti Sykäri
fonte
12
Ponto adicional sobre appendSuper (): você deve usá-lo em hashCode () e equals () se e somente se desejar herdar o comportamento de igualdade da superclasse. Por exemplo, se você derivar diretamente de Objeto, não faz sentido, porque todos os Objetos são distintos por padrão.
Antti Kissaniemi 28/05/09
312
Você pode fazer com que o Eclipse gere os dois métodos para você: Origem> Gerar hashCode () e igual a ().
Rok Strniša
27
Mesmo acontece com Netbeans: developmentality.wordpress.com/2010/08/24/...
seinecle
6
@Darthenius Eclipse iguais gerados usa getClass () que pode causar problemas em alguns casos (ver ponto Effective Java 8)
AndroidGecko
7
A primeira verificação nula não é necessária, dado que instanceofretorna false se seu primeiro operando for nulo (Java efetivo novamente).
izaban
295

Há alguns problemas que vale a pena notar se você estiver lidando com classes persistentes usando um ORM (Object-Relationship Mapper) como o Hibernate, se você não achou que isso já era irracionalmente complicado!

Objetos preguiçosos carregados são subclasses

Se seus objetos persistirem usando um ORM, em muitos casos, você estará lidando com proxies dinâmicos para evitar carregar o objeto muito cedo no armazenamento de dados. Esses proxies são implementados como subclasses de sua própria classe. Isso significa que this.getClass() == o.getClass()retornará false. Por exemplo:

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

Se você está lidando com um ORM, usar o instanceof Personé a única coisa que se comportará corretamente.

Objetos carregados preguiçosos têm campos nulos

Os ORMs geralmente usam os getters para forçar o carregamento de objetos carregados com preguiça. Isto significa que person.nameserá null, se personé preguiçoso carregado, mesmo se person.getName()as forças de carga e retorna "John Doe". Na minha experiência, isso surge mais frequentemente em hashCode()e equals().

Se você estiver lidando com um ORM, use sempre getters e nunca use referências de campo em hashCode()e equals().

Salvar um objeto mudará seu estado

Objetos persistentes geralmente usam um idcampo para manter a chave do objeto. Este campo será atualizado automaticamente quando um objeto for salvo pela primeira vez. Não use um campo de identificação em hashCode(). Mas você pode usá-lo equals().

Um padrão que costumo usar é

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

Mas: você não pode incluir getId()no hashCode(). Se o fizer, quando um objeto persistir, suas hashCodealterações. Se o objeto estiver em um HashSet, você "nunca" o encontrará novamente.

No meu Personexemplo, eu provavelmente usaria getName()for hashCodee getId()plus getName()(apenas para paranóia) para equals(). Tudo bem se houver algum risco de "colisões" hashCode(), mas nunca tudo bem equals().

hashCode() deve usar o subconjunto de propriedades não alteráveis ​​de equals()

Johannes Brodwall
fonte
2
@Johannes Brodwall: eu não entendo Saving an object will change it's state! hashCodedeve retornar int, então como você vai usar getName()? Você pode dar um exemplo para o seuhashCode
Jimmybondy
@jimmybondy: getName retornará um objeto String que também tem um hashCode que pode ser usado
mateusz.fiolka
85

Um esclarecimento sobre o obj.getClass() != getClass() .

Essa afirmação é o resultado de equals()ser uma herança hostil. O JLS (especificação da linguagem Java) especifica que, se for A.equals(B) == true, B.equals(A)também deve retornar true. Se você omitir essa instrução que herda classes que substituemequals() (e alteram seu comportamento) quebrará essa especificação.

Considere o seguinte exemplo do que acontece quando a instrução é omitida:

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

Fazendo new A(1).equals(new A(1))Além disso, o new B(1,1).equals(new B(1,1))resultado é verdadeiro, como deveria.

Parece tudo muito bom, mas veja o que acontece se tentarmos usar as duas classes:

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

Obviamente, isso está errado.

Se você deseja garantir a condição simétrica. a = b se b = a e o princípio de substituição de Liskov chamar super.equals(other)não apenas no caso de Bexemplo, mas verificar depois, por Aexemplo:

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;

Qual saída:

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

Onde, se anão for uma referência B, pode ser uma referência à classe A(porque você a estende), nesse caso você super.equals() também chama .

Ran Biron
fonte
2
Você pode fazer igual a simétrico dessa maneira (se comparar um objeto de superclasse com objeto de subclasse, sempre use os iguais da subclasse) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (this);
pihentagy
5
@pihentagy - então eu recebia um stackoverflow quando a classe de implementação não substitui o método equals. não tem graça.
Ran Biron
2
Você não receberá um fluxo de pilha. Se o método equals não for substituído, você chamará o mesmo código novamente, mas a condição de recursão será sempre falsa!
Jacob Raihle
@pihentagy: Como isso se comporta se houver duas classes derivadas diferentes? Se a ThingWithOptionSetApuder ser igual a uma, Thingdesde que todas as opções extras tenham valores padrão, e da mesma forma para a ThingWithOptionSetB, será possível ThingWithOptionSetAcomparar a igual a a ThingWithOptionSetBapenas se todas as propriedades não básicas de ambos os objetos corresponderem aos padrões, mas Não vejo como você testa isso.
Supercat 28/13 /
7
O problema com isso é que ele quebra a transitividade. Se você adicionar B b2 = new B(1,99), então b.equals(a) == truee a.equals(b2) == truemas b.equals(b2) == false.
nickgrim
46

Para uma implementação amigável à herança, confira a solução de Tal Cohen, Como implemento corretamente o método equals ()?

Resumo:

Em seu livro Effective Java Programming Language Guide (Addison-Wesley, 2001), Joshua Bloch afirma que "simplesmente não há como estender uma classe instável e adicionar um aspecto, preservando o contrato igual". Tal discorda.

Sua solução é implementar equals () chamando outro não-simétrico blindlyEquals () nos dois sentidos. blindlyEquals () é substituído por subclasses, equals () é herdado e nunca substituído.

Exemplo:

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

Observe que equals () deve funcionar em hierarquias de herança para que o Princípio de Substituição de Liskov seja atendido.

Kevin Wong
fonte
10
Dê uma olhada no método canEqual explicado aqui - o mesmo princípio faz as duas soluções funcionarem, mas com o canEqual você não compara os mesmos campos duas vezes (acima, px == this.x será testado em ambas as direções): artima.com /lejava/articles/equality.html
Blaisorblade
2
De qualquer forma, não acho que seja uma boa ideia. Isso torna desnecessariamente confuso o contrato Igual - alguém que usa dois parâmetros de ponto, a e b, deve estar consciente da possibilidade de que a.getX () == b.getX () e a.getY () == b.getY () pode ser verdadeiro, mas a.equals (b) e b.equals (a) podem ser falsos (se apenas um for um ColorPoint).
Kevin
Basicamente, é assim if (this.getClass() != o.getClass()) return false, mas flexível, na medida em que só retorna false se a (s) classe (s) derivada (s) se preocupam em modificar iguais. Isso está certo?
Aleksandr Dubinsky,
31

Ainda espantado que ninguém recomendasse a biblioteca de goiaba para isso.

 //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());
    }
Eugene
fonte
23
java.util.Objects.hash () e java.util.Objects.equals () fazem parte do Java 7 (lançado em 2011), portanto você não precisa do Guava para isso.
herman
1
é claro, mas você deve evitar isso, já que a Oracle não está mais fornecendo atualizações públicas para Java 6 (esse é o caso desde fevereiro de 2013).
herman
6
Seu thisem this.getDate()meios nada (excepto desordem)
Steve Kuo
1
Sua expressão "não instanceof" precisa de um suporte adicional: if (!(otherObject instanceof DateAndPattern)) {. Concordo com hernan e Steve Kuo (apesar de ser uma questão de preferência pessoal), mas com +1.
Amos M. Carpenter
26

Existem dois métodos na superclasse como java.lang.Object. Precisamos substituí-los pelo objeto personalizado.

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

Objetos iguais devem produzir o mesmo código de hash, desde que iguais, porém objetos desiguais não precisam produzir códigos de 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
}

Se você quiser obter mais, consulte este link como http://www.javaranch.com/journal/2002/10/equalhash.html

Este é outro exemplo, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Diverta-se! @. @

Luna Kong
fonte
Desculpe, mas não entendo esta afirmação sobre o método hashCode: não é legal se ele usa mais variáveis ​​que equals (). Mas se eu codifico com mais variáveis, meu código é compilado. Por que isso não é legal?
Adryr83
19

Existem algumas maneiras de verificar sua igualdade de classe antes de verificar a igualdade de membros, e acho que ambas são úteis nas circunstâncias certas.

  1. Use o instanceofoperador.
  2. Use this.getClass().equals(that.getClass()).

Uso o número 1 em uma finalimplementação igual ou ao implementar uma interface que prescreve um algoritmo para iguais (como as java.utilinterfaces de coleta - a maneira correta de verificar com(obj instanceof Set) ou interface que você está implementando). Geralmente é uma má escolha quando os iguais podem ser substituídos porque isso quebra a propriedade de simetria.

A opção 2 permite que a classe seja estendida com segurança, sem anular iguais ou quebrar a simetria.

Se sua classe também for Comparable, os métodos equalse também compareTodevem ser consistentes. Aqui está um modelo para o método equals em uma Comparableclasse:

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

}
erickson
fonte
1
+1 para isso. Nem getClass () nem instanceof são uma panacéia, e esta é uma boa explicação de como abordar os dois. Não pense que há algum motivo para não fazer isso.getClass () == that.getClass () em vez de usar equals ().
Paul Cantrell
Há um problema com isso. Classes anônimas que não adicionam nenhum aspecto nem substituem o método equals falharão na verificação de getClass, mesmo que sejam iguais.
steinybot
@ Stiny Não está claro para mim que objetos de tipos diferentes devem ser iguais; Estou pensando em diferentes implementações de uma interface como uma classe anônima comum. Você pode dar um exemplo para apoiar sua premissa?
precisa
MyClass a = new MyClass (123); MyClass b = new MyClass (123) {// Substituir algum método}; // a.Equals (b) é falso quando se utiliza this.getClass () é igual a (that.getClass ()).
steinybot
1
@Steiny Right. Como deveria na maioria dos casos, especialmente se um método for substituído em vez de adicionado. Considere o meu exemplo acima. Se não final, e o compareTo()método foi substituído para reverter a ordem de classificação, as instâncias da subclasse e superclasse não devem ser consideradas iguais. Quando esses objetos foram usados ​​juntos em uma árvore, as chaves "iguais" de acordo com uma instanceofimplementação podem não ser encontradas.
21715
16

Para iguais, procure Secrets of Equals de Angelika Langer . Eu amo muito isso. Ela também é uma ótima FAQ sobre genéricos em Java . Veja seus outros artigos aqui (role para baixo até "Core Java"), onde ela também continua com a Parte 2 e a "comparação de tipos mistos". Divirta-se lendo-os!

Johannes Schaub - litb
fonte
11

O método equals () é usado para determinar a igualdade de dois objetos.

como valor int de 10 é sempre igual a 10. Mas esse método equals () trata da igualdade de dois objetos. Quando dizemos objeto, ele terá propriedades. Para decidir sobre igualdade, essas propriedades são consideradas. Não é necessário que todas as propriedades sejam levadas em consideração para determinar a igualdade e, com relação à definição e contexto da classe, ela pode ser decidida. Em seguida, o método equals () pode ser substituído.

devemos sempre substituir o método hashCode () sempre que substituirmos o método equals (). Se não, o que vai acontecer? Se usarmos hashtables em nosso aplicativo, ele não se comportará conforme o esperado. Como o hashCode é usado na determinação da igualdade de valores armazenados, ele não retornará o valor correspondente correto para uma chave.

A implementação padrão fornecida é o método hashCode () na classe Object, que usa o endereço interno do objeto e o converte em número inteiro e o retorna.

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;

  }
}

Exemplo de saída de código:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966
Rohan Kamat
fonte
7

Logicamente, temos:

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

Mas não vice-versa!

Khaled.K
fonte
6

Um problema que encontrei é que dois objetos contêm referências um ao outro (um exemplo é um relacionamento pai / filho com um método de conveniência no pai para obter todos os filhos).
Esses tipos de coisas são bastante comuns ao fazer mapeamentos do Hibernate, por exemplo.

Se você incluir as duas extremidades do relacionamento em seu hashCode ou em testes iguais, é possível entrar em um loop recursivo que termina em StackOverflowException.
A solução mais simples é não incluir a coleção getChildren nos métodos.

Grevas de Darren
fonte
5
Penso que a teoria subjacente aqui é distinguir entre os atributos , agregados e associatinos de um objeto. As associações não devem participar equals(). Se um cientista louco criou uma duplicata de mim, seríamos equivalentes. Mas não teríamos o mesmo pai.
Raedwald 29/09