Identidade (==) vs igualdade lógica (equals)
Em Java, == compara identidade: se duas referências apontam para o mesmo objeto na memória. Já equals compara igualdade lógica: se dois objetos devem ser considerados equivalentes segundo uma regra de negócio.
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b); // false (objetos diferentes)
System.out.println(a.equals(b)); // true (mesmo conteúdo)
Regra prática: use == quando você quer saber se é o mesmo objeto; use equals quando você quer saber se “representam a mesma coisa”.
Quando equals não é sobrescrito
Se uma classe não sobrescreve equals, ela herda a implementação de Object, que é essencialmente baseada em identidade (equivalente a ==). Isso costuma ser incorreto para classes que representam valores (ex.: dinheiro, e-mail, coordenadas) e muitas entidades de domínio.
O contrato de equals: regras que sua implementação deve obedecer
Uma implementação correta de equals deve respeitar o contrato:
- Reflexivo:
x.equals(x)étrue. - Simétrico:
x.equals(y)implicay.equals(x). - Transitivo: se
x.equals(y)ey.equals(z), entãox.equals(z). - Consistente: múltiplas chamadas retornam o mesmo resultado enquanto os campos usados na comparação não mudarem.
- Não-nulo:
x.equals(null)éfalse.
Essas regras não são “teoria”: elas garantem que coleções e algoritmos consigam confiar na comparação.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
A relação obrigatória entre equals e hashCode
Além do contrato de equals, existe uma regra obrigatória envolvendo hashCode:
- Se
a.equals(b)étrue, entãoa.hashCode() == b.hashCode()deve sertrue.
O inverso não é obrigatório: dois objetos podem ter o mesmo hashCode e ainda assim não serem iguais (colisões).
Por que isso importa em HashMap/HashSet
HashMap e HashSet usam hashCode para escolher um “balde” (bucket) e depois usam equals para confirmar se é o mesmo elemento/chave. Se equals e hashCode não estiverem alinhados, você terá sintomas como:
set.contains(obj)retornandofalsepara algo que “está no set”.map.get(chave)retornandonullmesmo com a chave “equivalente”.- duplicatas em
HashSetquando não deveria.
Passo a passo: implementando equals e hashCode corretamente
A seguir, um roteiro prático que funciona bem na maioria dos casos.
Passo 1: defina o que significa “igual” para a classe
Antes do código, decida quais campos determinam igualdade. Isso depende do papel do objeto:
- Value Object: igualdade normalmente é por todos os campos relevantes do valor.
- Entidade: igualdade normalmente é por identidade de negócio (um identificador estável), não por todos os atributos mutáveis.
Passo 2: implemente equals com checagens padrão
Estrutura típica:
- Se
this == o, retornetrue. - Se
o == nullou classes incompatíveis, retornefalse. - Faça cast e compare os campos escolhidos.
Passo 3: implemente hashCode com os mesmos campos
Use os mesmos campos usados no equals. Uma forma segura e legível é Objects.hash(...) (com custo aceitável na maioria dos domínios). Para cenários de altíssima performance, pode-se usar cálculo manual, mas a prioridade aqui é correção.
Exemplo 1: Value Object (imutável) com equals/hashCode por valor
Um value object como Money deve considerar iguais valores com mesma quantia e moeda.
import java.math.BigDecimal;
import java.util.Objects;
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = Objects.requireNonNull(amount);
this.currency = Objects.requireNonNull(currency);
}
public BigDecimal amount() { return amount; }
public String currency() { return currency; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
Como é imutável, o resultado de equals/hashCode não muda com o tempo, o que é ideal para uso em HashSet/HashMap.
Armadilha comum: BigDecimal e igualdade
BigDecimal.equals considera escala. Assim, new BigDecimal("10.0") não é igual a new BigDecimal("10.00"). Se no seu domínio isso deve ser considerado igual, você precisa normalizar (ex.: stripTrailingZeros()) ou comparar via compareTo e ajustar o hashCode de forma consistente. Caso contrário, você terá “duplicatas” inesperadas em sets.
Exemplo 2: Entidade com equals/hashCode por identificador estável
Em entidades, comparar todos os atributos pode ser perigoso, porque muitos mudam ao longo do ciclo de vida. O mais comum é usar um identificador estável (por exemplo, id).
import java.util.Objects;
public class Customer {
private final String id; // identidade estável
private String name;
private String email;
public Customer(String id, String name, String email) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.email = Objects.requireNonNull(email);
}
public String id() { return id; }
public String name() { return name; }
public String email() { return email; }
public void changeEmail(String newEmail) {
this.email = Objects.requireNonNull(newEmail);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id.equals(customer.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Note que name e email não entram na igualdade. Isso evita que o objeto “mude de balde” em um HashSet quando o e-mail for alterado.
Armadilha: id nulo ou id que muda
Se o id puder ser null em parte do ciclo de vida (ex.: antes de persistir) ou puder mudar, você pode quebrar coleções baseadas em hash. Estratégias comuns:
- Gerar o id no construtor (UUID) para ser estável desde o início.
- Não colocar instâncias “sem id” em
HashSet/HashMapcomo chave. - Evitar setters para id.
Uso correto em coleções: cenários práticos
HashSet: evitando duplicatas por igualdade lógica
import java.util.HashSet;
import java.util.Set;
Set<Money> prices = new HashSet<>();
prices.add(new Money(new BigDecimal("10.00"), "BRL"));
prices.add(new Money(new BigDecimal("10.00"), "BRL"));
System.out.println(prices.size()); // 1 (se equals/hashCode corretos)
HashMap: chave lógica e recuperação consistente
import java.util.HashMap;
import java.util.Map;
Map<Customer, String> notes = new HashMap<>();
Customer c1 = new Customer("C-10", "Ana", "ana@ex.com");
notes.put(c1, "VIP");
Customer c2 = new Customer("C-10", "Ana Maria", "ana2@ex.com");
System.out.println(notes.get(c2)); // "VIP" (mesmo id => equals true)
Esse comportamento é desejável quando a identidade da entidade é o id.
Bug clássico: campos mutáveis usados em equals/hashCode
O erro mais comum é incluir no hashCode um campo que pode mudar após o objeto ser inserido em um HashSet ou usado como chave em HashMap.
Exemplo de implementação incorreta
import java.util.Objects;
public class User {
private String email; // mutável
public User(String email) {
this.email = email;
}
public void changeEmail(String newEmail) {
this.email = newEmail;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}
Como o bug aparece
import java.util.HashSet;
import java.util.Set;
User u = new User("a@ex.com");
Set<User> set = new HashSet<>();
set.add(u);
u.changeEmail("b@ex.com");
System.out.println(set.contains(u)); // pode ser false
System.out.println(set.remove(u)); // pode ser false
O objeto foi armazenado em um bucket calculado com o hash antigo. Ao mudar o e-mail, o hash muda, e a coleção procura no bucket errado.
Correções possíveis
- Tornar o campo usado em
equals/hashCodeimutável. - Em entidades, basear igualdade em um id estável e imutável.
- Evitar usar objetos mutáveis como chave de
HashMapou elemento deHashSet.
Quais campos entram na comparação? Heurísticas úteis
| Tipo de objeto | Campos típicos em equals/hashCode | Evitar |
|---|---|---|
| Value Object (ex.: Money, Email) | Todos os campos que definem o valor | Campos derivados/redundantes; dados transitórios |
| Entidade (ex.: Customer, Order) | Identificador estável (id de negócio ou técnico) | Atributos que mudam (nome, status, timestamps) |
| DTO/Request | Depende do uso; muitas vezes não precisa sobrescrever | Implementar sem necessidade e criar semântica confusa |
getClass() vs instanceof em equals
Há duas abordagens comuns:
getClass(): só considera iguais objetos da mesma classe exata. Evita problemas de simetria/transitividade com herança.instanceof: permite igualdade entre subclasses, mas pode quebrar o contrato se subclasses adicionarem estado relevante.
Em modelos com herança, é fácil errar. Uma prática segura é preferir getClass() quando você não tem uma estratégia bem definida para igualdade em hierarquias.
Testes: verificando o contrato e prevenindo regressões
Testes automatizados ajudam a detectar violações do contrato e bugs em coleções. Abaixo, exemplos com JUnit 5.
Teste de propriedades básicas do contrato
import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class MoneyEqualityTest {
@Test
void equalsContract_basicProperties() {
Money x = new Money(new BigDecimal("10.00"), "BRL");
Money y = new Money(new BigDecimal("10.00"), "BRL");
Money z = new Money(new BigDecimal("10.00"), "BRL");
// reflexivo
assertEquals(x, x);
// simétrico
assertEquals(x.equals(y), y.equals(x));
// transitivo
assertTrue(x.equals(y) && y.equals(z) && x.equals(z));
// não-nulo
assertNotEquals(x, null);
// hashCode consistente com equals
assertEquals(x, y);
assertEquals(x.hashCode(), y.hashCode());
}
}
Teste de comportamento em HashSet (detecção de hashCode incorreto)
import static org.junit.jupiter.api.Assertions.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
class CustomerHashSetTest {
@Test
void hashSetShouldNotDuplicateSameIdentity() {
Customer c1 = new Customer("C-10", "Ana", "ana@ex.com");
Customer c2 = new Customer("C-10", "Ana Maria", "ana2@ex.com");
Set<Customer> set = new HashSet<>();
set.add(c1);
set.add(c2);
assertEquals(1, set.size());
}
}
Teste que evidencia bug com campo mutável na chave
import static org.junit.jupiter.api.Assertions.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
class MutableKeyBugTest {
@Test
void changingFieldUsedInHashBreaksHashSet() {
User u = new User("a@ex.com");
Set<User> set = new HashSet<>();
set.add(u);
u.changeEmail("b@ex.com");
// Este teste pode falhar (e é exatamente o problema)
assertFalse(set.contains(u));
}
}
Esse último teste é útil como “teste de demonstração” para a equipe: ele mostra por que chaves/elementos em coleções hash devem ter identidade estável.
Cenário de bug realista: HashMap com chave inconsistente
Um erro frequente é sobrescrever equals e esquecer de sobrescrever hashCode (ou implementá-lo com campos diferentes). O resultado: inserção funciona, mas recuperação falha.
import java.util.HashMap;
import java.util.Map;
class ProductCode {
private final String code;
ProductCode(String code) { this.code = code; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProductCode that = (ProductCode) o;
return code.equals(that.code);
}
// BUG: hashCode não sobrescrito => herda de Object (identidade)
}
Map<ProductCode, Integer> stock = new HashMap<>();
stock.put(new ProductCode("P-1"), 10);
System.out.println(stock.get(new ProductCode("P-1"))); // null (bug)
Correção: sobrescrever hashCode usando o mesmo campo code.