Quais questões devem ser consideradas ao substituir equals e hashCode em Java?
Pergunta
Quais problemas/armadilhas devem ser considerados ao substituir equals
e hashCode
?
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ãoa.hashCode()
deve ser igual ab.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.
- Use o
instanceof
operador. - 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.