O operador == sempre compara a identidade, assim como a implementação default do método equals.
As vezes a implementação default do equals possui o comportamento desejado (como por exemplo numa type-safe enumeration), mas o equals deveria geralmente comparar o estado, não a identidade. Isso é particularmente verdade para classes data-centric que mapeiam registros de bancos de dados.
hashCode e equals possuem uma relação próxima:
-
se voce sobrescreve equals, voce deve sobrescrever também o hashCode.
-
hashCode deve ser igual para objetos equals entre si.
-
equals e hashCode devem depender do mesmo conjunto de campos. Não é necessário utilizar todos os campos, somente os campos significativos. Por exemplo, um campo que é calculado a partir de outros campos, poderia ser omitido do equals e hashCode.
Objetos inseridos em Lists, Sets, ou Maps (tanto como chave quanto como valor) deveriam ter uma definição apropriada do equals.
Se você extende uma classe concreta e adiciona um novo campo que contribui para o equals, então não é possível escrever um equals perfeitamente correto para esta classe nova. Ao invés disso, você deveria usar composição ao invés de herança.
Ao implementar o equals, os campos são comparados de forma diferente dependendo do seu tipo:
-
campos do tipo object, incluindo collections: usam equals
-
type-safe enumerations: usam == e equals pois instancias diferentes podem conter os mesmos objetos
-
campos object possivelmente null: usam == e equals
-
campos de tipos primitivos diferentes de float ou double: usam ==
-
campos float: são convetidos para int utilizando Float.floatToIntBits e em seguida comparados com ==
-
campos double: são convertidos para long utilizando Double.doubleToLongBits e em seguida comparados com ==
-
campos do tipo array: comparam cada elemento do array aplicando as regras acima
É importante notar que se um campo é do tipo wrapper class, então a implementação do equals é simples, pois ela só faz uma coisa: delega a invocação do equals.
Em um método equals é, geralmente, melhor ordenar as comparações dos campos de acordo com a significância do campo, isto é, campos mais significativos, deveriam ser avaliados primeiro. Isso permite que o operador lógico && minimize o tempo de execução.
Abaixo temos um exemplo padrão de implementação do método equals que leva em consideração cada atributo, este código é análogo aos algoritmos gerados pelas IDE's eclipse e NetBeans.
import java.util.Arrays;
class MinhaClasse {
private int atributo1;
private Object atributo2;
private String[] atributo3;
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof MinhaClasse)) {
return false;
}
MinhaClasse other = (MinhaClasse) obj;
if (atributo1 != other.atributo1) {
return false;
}
if (atributo2 == null) {
if (other.atributo2 != null) {
return false;
}
} else if (!atributo2.equals(other.atributo2)) {
return false;
}
if (!Arrays.equals(atributo3, other.atributo3)) {
return false;
}
return true;
}
}
Percebam que é um código bastante extenso e sua manutenção exige bastante atenção, principalmente se a classe for alterada para possuir mais atributos que possam influenciar na computação do equals.
Tendo em vista as peculiaridades acerca do método equals, disponibilizei em meu repositório a classe EqualsUtils que simplifica a implementação do método equals.
Abaixo, um exemplo prático de como utilizar a classe EqualsUtils para implementar o método equals da classe acima:
import static br.com.staroski.equality.EqualsUtils.*;
class MinhaClasse {
private int atributo1;
private Object atributo2;
private String[] atributo3;
public boolean equals(Object object){
if (this == object) {
return true;
}
if (object instanceof MinhaClasse) {
MinhaClasse that = (MinhaClasse) object;
return equal(this.atributo1, that.atributo1)
&& equal(this.atributo2, that.atributo2)
&& equal(this.atributo3, that.atributo3);
}
return false;
}
}