Pergunta

Quais problemas/armadilhas devem ser considerados ao substituir equals e hashCode?

Foi útil?

Solução

A teoria (para os advogados linguísticos e os inclinados à matemática):

equals() (javadoc) deve definir uma relação de equivalência (deve ser reflexivo, simétrico, e transitivo).Além disso, deve ser consistente (se os objetos não forem modificados, ele deverá 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).

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

Em qualquer momento a.equals(b), então a.hashCode() deve ser igual a b.hashCode().

Na prática:

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

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

Use as excelentes classes auxiliares Construtor igual e Construtor HashCode de Apache Commons Lang biblioteca.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 um baseado em hash Coleção ou Mapa como HashSet, LinkedHashSet, HashMap, Tabela hash, ou FracoHashMap, certifique-se de que o hashCode() dos objetos-chave que você colocou na coleção nunca mude enquanto o objeto estiver na coleção.A maneira infalível de garantir isso é tornar suas chaves imutáveis, que também tem outros benefícios.

Outras dicas

Existem alguns problemas que vale a pena observar se você estiver lidando com classes que são persistidas usando um Mapeador de Relacionamento de Objetos (ORM) como o Hibernate, se você não acha que isso já é excessivamente complicado!

Objetos carregados lentamente são subclasses

Se seus objetos forem persistidos 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 quethis.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ê estiver lidando com um ORM, usando o instanceof Person é a única coisa que se comportará corretamente.

Objetos carregados lentamente têm campos nulos

ORMs geralmente usam getters para forçar o carregamento de objetos carregados lentamente.Isso significa que person.name vai ser null se person é carregado lentamente, mesmo que person.getName() força o carregamento e retorna "John Doe".Na minha experiência, isso ocorre com mais frequência em hashCode() e equals().

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

Salvar um objeto mudará seu estado

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

Um padrão que uso com frequência é

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

Mas:você não pode incluir getId() em hashCode().Se você fizer isso, quando um objeto for persistido, seu hashCode mudanças.Se o objeto estiver em um HashSet, você "nunca" o encontrará novamente.

No meu Person por exemplo, eu provavelmente usaria getName() para hashCode e getId() mais getName() (apenas por paranóia) para equals().Não há problema se houver algum risco de "colisões" para hashCode(), mas nunca está bem para equals().

hashCode() deve usar o subconjunto imutável de propriedades de equals()

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

Esta afirmação é o resultado de equals() sendo hostil à herança.O JLS (especificação da linguagem Java) especifica que se A.equals(B) == true então B.equals(A) também deve retornar true.Se você omitir essa instrução herdando classes que substituem equals() (e mudar seu comportamento) quebrará esta especificação.

Considere o seguinte exemplo do que acontece quando a declaraçã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)) Também, new B(1,1).equals(new B(1,1)) o 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ê quiser garantir a condição simétrica.a=b se b=a e a chamada do princípio de substituição de Liskov super.equals(other) não só no caso de B por exemplo, mas verifique depois A instância:

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;

O que produzirá:

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

Onde, se a não é uma referência de B, então pode ser uma referência de classe A (porque você estende), neste caso você chama super.equals() também.

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

Resumo:

Em seu livro Guia eficaz da linguagem de programação Java (Addison-Wesley, 2001), Joshua Bloch afirma que "simplesmente não há como estender uma classe instantável e adicionar um aspecto enquanto preservava o contrato igual". Tal discorda.

Sua solução é implementar equals() chamando outro blindlyEquals() assimétrico 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 se o Princípio da Substituição de Liskov é ficar satisfeito.

Ainda estou surpreso que ninguém tenha recomendado 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());
    }

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 hash desde que sejam iguais, porém objetos desiguais não precisam produzir 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
}

Se você quiser obter mais, verifique 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

Divirta-se!@.@

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

  1. Use o instanceof operador.
  2. Usar this.getClass().equals(that.getClass()).

Eu uso o número 1 em um final implementação de igual, ou ao implementar uma interface que prescreve um algoritmo para igual (como o java.util interfaces de coleção - a maneira certa de verificar com (obj instanceof Set) ou qualquer interface que você esteja implementando).Geralmente é uma má escolha quando iguais podem ser substituídos porque isso quebra a propriedade de simetria.

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

Se sua turma também for Comparable, o equals e compareTo os métodos também devem ser consistentes.Aqui está um modelo para o método equals em um Comparable aula:

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

}

Para iguais, olhe para Segredos de Iguais por Angélica Langer.Eu amo muito isso.Ela também é uma ótima FAQ sobre Genéricos em Java.Veja outros artigos dela aqui (role para baixo até "Core Java"), onde ela também continua com a Parte 2 e "comparação de tipos mistos".Divirta-se lendo-os!

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

como o valor int de 10 é sempre igual a 10.Mas este método equals() trata da igualdade de dois objetos.Quando dizemos objeto, ele terá propriedades.Para decidir sobre a 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 de classe e ao contexto ela pode ser decidida.Então 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 acontecerá?Se usarmos hashtables em nossa aplicação, ela não se comportará conforme o esperado.Como o hashCode é usado para determinar a igualdade dos 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 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

Logicamente temos:

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

Mas não vice-versa!

Uma pegadinha que encontrei é onde 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).
Esse tipo de coisa é bastante comum ao fazer mapeamentos do Hibernate, por exemplo.

Se você incluir ambas as extremidades do relacionamento em seus testes hashCode ou equals, é 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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top