1. Introdução
Como comparamos objetos
Em Java, objetos — não são apenas dados, cada um tem seu próprio endereço na memória. O operador == responde à pergunta “é a mesma caixinha?”, ou seja, compara referências (endereços), não o conteúdo. O método equals destina-se a comparar justamente o conteúdo. Por padrão, se não for sobrescrito, equals se comporta como ==.
Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);
System.out.println(p1 == p2); // false — são objetos diferentes na memória!
Para que dois objetos diferentes sejam considerados iguais pelos dados (por exemplo, todos os campos significativos coincidem), é necessário sobrescrever equals. E, se você planeja usar os objetos em coleções baseadas em hash, é obrigatório sobrescrever corretamente também o hashCode.
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // é o mesmo objeto
if (o == null || getClass() != o.getClass()) return false; // verificamos a classe
Person person = (Person) o; // fazemos o cast para o tipo necessário
return age == person.age && name.equals(person.name); // comparamos os campos
}
@Override
public int hashCode() {
return Objects.hash(name, age); // para funcionar com HashSet/HashMap
}
}
Tal sobrescrita é crítica para o funcionamento correto com HashSet/HashMap: sem equals e hashCode, as coleções considerarão diferentes até mesmo objetos iguais pelos dados.
A relação de equivalência em equals depende dos seus requisitos: é possível comparar por todos os campos, por parte deles (por exemplo, apenas o email em User) — o importante é a consistência e o cumprimento do contrato.
Onde isso é especialmente importante?
- Em coleções baseadas em tabelas de hash: HashSet, HashMap, LinkedHashSet e outros.
- Ao buscar e remover elementos nas coleções: sem um equals correto, o objeto desejado pode “não ser encontrado”.
- Na lógica de negócio: por exemplo, dois User com o mesmo e-mail devem ser considerados o mesmo usuário.
hashCode — para que ele serve?
Coleções baseadas em hash (por exemplo, HashSet, HashMap) usam tabelas de hash. O método hashCode calcula um número inteiro — o “endereço do bucket” para onde o objeto irá. Se dois objetos são iguais de acordo com equals, seus hashCode devem coincidir. Se você quebrar essa regra, as coleções começarão a se comportar de forma imprevisível.
2. Contrato de equals e hashCode
Contrato de equals
Requisitos principais para o comportamento de equals:
- Reflexividade: a.equals(a) é sempre true.
- Simetria: se a.equals(b) é true, então b.equals(a) também é true.
- Transitividade: se a.equals(b) e b.equals(c), então a.equals(c) também é true.
- Consistência: se os objetos não mudam, o resultado das chamadas permanece estável.
- Comparação com null: qualquer objeto não é igual a null.
Contrato de hashCode
- Se dois objetos são iguais por equals, seus hashCode são iguais.
- Se os objetos não são iguais, seus hash codes podem coincidir (colisões são permitidas, mas indesejáveis).
- Enquanto o objeto não muda logicamente, seu hashCode deve permanecer constante.
Em outras palavras, hashCode coincidente é condição necessária, mas não suficiente de igualdade: o mesmo hash não garante igualdade por equals.
3. Implementação de equals e hashCode: exemplo
Considere a classe Person, em que a igualdade é definida pelos campos name e age.
public class Person {
private String name;
private int age;
// Construtor, getters, setters...
@Override
public boolean equals(Object o) {
if (this == o) return true; // Comparação de referências
if (o == null || getClass() != o.getClass()) return false; // Verificação da classe
Person person = (Person) o; // Conversão de tipo
// Comparamos os campos
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 — uma escolha comum de número primo
return result;
}
}
- Primeiro, verificações rápidas: referência e classe.
- Depois, a comparação dos campos significativos.
- Em hashCode, usa-se o número primo 31 para reduzir colisões.
Uso de Objects.equals e Objects.hash
A partir do Java 7, a classe Objects simplifica o código e o torna mais seguro em relação a null:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals, hashCode e compareTo: como eles se relacionam
Como equals e compareTo estão relacionados?
A interface Comparable define o método compareTo, que retorna um número negativo/zero/positivo para “menor/igual/maior”. É desejável que de a.compareTo(b) == 0 se siga a.equals(b). O inverso não é obrigatório.
Se essa consistência for violada (por exemplo, compareTo compara apenas por idade, enquanto equals compara por nome e idade), coleções ordenadas como TreeSet/TreeMap podem se comportar de forma inesperada: os objetos são considerados “iguais” do ponto de vista da ordenação, mas não iguais pelo conteúdo.
equals e hashCode em coleções
- Em HashSet e HashMap, as operações de adicionar/buscar/remover dependem da implementação correta de equals e hashCode.
- Sem sobrescrever, essas coleções consideram os objetos “diferentes”, mesmo que seus dados sejam iguais.
5. Exemplos: como isso funciona nas coleções
HashSet: armazenamento de objetos únicos
Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplicado
System.out.println(people.size()); // 1, se equals/hashCode estiverem implementados corretamente
Sem um equals/hashCode corretos, ambos os objetos entrarão no conjunto.
HashMap: busca por chave
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
map.put(p1, "Usuário 1");
System.out.println(map.get(p2)); // "Usuário 1", se equals/hashCode estiverem implementados corretamente
Sem o contrato, a coleção retornará null — para ela, são “chaves diferentes”.
6. Boas práticas: dicas de implementação
- Inclua em equals/hashCode todos os campos que definem a “identidade” do objeto.
- Não use campos mutáveis (que mudam após a inserção nas coleções) no cálculo do hashCode.
- Deixe a IDE gerar os métodos — menos chances de erros de digitação.
- Em equals, verifique primeiro this == o, depois a classe, e então os campos.
- Para comparar campos-objeto, use Objects.equals.
- Para o hash code, use Objects.hash ou um padrão consagrado com multiplicador 31.
7. Nuances úteis
Por que não dá para usar apenas hashCode?
Colisões são inevitáveis: objetos diferentes podem ter o mesmo hashCode. O hash é apenas um direcionamento rápido para o bucket; a decisão final sobre igualdade é tomada por equals.
É possível não sobrescrever equals e hashCode?
Somente se você tiver certeza de que os objetos nunca serão comparados por conteúdo e não serão chaves/elementos únicos em coleções. Na prática, isso é raro.
Diferença entre ==, equals e compareTo
| Operador/método | O que compara? | Para que serve? |
|---|---|---|
|
Referências (endereços na memória) | Verificar “é o mesmo objeto?” |
|
Conteúdo dos objetos | Igualdade segundo a lógica de negócio |
|
Ordem (menor/igual/maior) | Ordenação |
8. Erros típicos ao implementar equals e hashCode
Erro nº 1: sobrescreveu equals, mas esqueceu hashCode. Os objetos são considerados iguais, mas caem em buckets diferentes da tabela de hash — busca e remoção quebram.
Erro nº 2: usar campos mutáveis em hashCode. Se um campo mudar após a inserção na coleção, o objeto “se perde”: o hash muda, mas o bucket não.
Erro nº 3: simetria/transitividade de equals violada. a.equals(b) dá true, mas b.equals(a) — false, ou a transitividade quebra — as coleções começam a se comportar de forma imprevisível.
Erro nº 4: não verificar a classe em equals. Comparar objetos de classes diferentes leva a resultados incorretos ou exceções.
Erro nº 5: comparar strings e objetos com ==. O operador == compara referências; use equals para o conteúdo.
Erro nº 6: falta de consistência entre compareTo e equals. Se a.compareTo(b) == 0, mas !a.equals(b), as coleções TreeSet/TreeMap podem considerar elementos iguais para a ordenação, mas diferentes quanto à igualdade — isso é fonte de “fantasmas” e duplicatas.
GO TO FULL VERSION