A diferença entre dois objetos serem iguais e serem o mesmo objeto, como ensinar isso às suas classes, e o contrato que o equals e o hashCode precisam cumprir juntos.
Esta pergunta vem se acumulando desde "Operadores, Texto e Conversa com o Usuário", quando dois textos idênticos deram false com ==. Em "Muitos Valores sob um Nome", o mesmo aconteceu com arrays. E em "Do Geral ao Específico" apareceu o método equals herdado de Object, com a promessa de uma aula própria. É esta. Você vai ver que a igualdade entre objetos é uma decisão de projeto, que o Java a expressa por um contrato de dois métodos, equals e hashCode, e que respeitar só metade desse contrato produz alguns dos erros mais difíceis de diagnosticar da linguagem.
Identidade e igualdade
Há duas perguntas diferentes que se pode fazer sobre dois objetos:
- São o mesmo objeto? Essa é a pergunta da identidade, respondida pelo
==, que compara referências. - Representam a mesma coisa? Essa é a pergunta da igualdade, respondida pelo
equals, conforme o que a classe definir.
Duas notas de 10 reais são iguais: têm o mesmo valor, e ninguém se importa com qual delas recebe de troco. Mas não são a mesma nota. Cada uma tem seu número de série. Para a maioria dos objetos que representam valores, como pontos, datas, quantias e textos, o que importa é a igualdade. A identidade é um detalhe de onde o objeto está na memória.
O equals herdado de Object responde, por padrão, a pergunta da identidade:
public boolean equals(Object obj) {
return (this == obj);
}
Por isso, uma classe que não sobrescreve equals considera iguais apenas objetos que são o mesmo. Para a classe Ponto de "O que Fica Dentro, o que Fica Fora", dois pontos com as mesmas coordenadas são diferentes, o que não faz sentido para um valor. Vamos corrigir isso.
O contrato do equals
A documentação de Object exige que toda sobrescrita de equals seja:
- Reflexiva:
x.equals(x)é sempre verdadeiro. - Simétrica: se
x.equals(y), entãoy.equals(x). - Transitiva: se
x.equals(y)ey.equals(z), entãox.equals(z). - Consistente: chamadas repetidas dão o mesmo resultado, enquanto os objetos não mudam.
- Segura com null:
x.equals(null)é sempre falso, e nunca lança exceção.
Parecem óbvias, e é fácil violá-las sem perceber, como você vai ver adiante. A importância do contrato está no fato de que todo o resto da biblioteca, como as coleções, as buscas e as remoções, depende dele. Uma coleção não tem como funcionar direito com objetos cujo equals é inconsistente.
Implementando equals
A versão do Ponto com equals:
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Ponto outro)) {
return false;
}
return Double.compare(x, outro.x) == 0
&& Double.compare(y, outro.y) == 0;
}
Linha por linha:
- O parâmetro é
Object, e nãoPonto. É a assinatura deObject.equals, e só com ela há sobrescrita. Umequals(Ponto outro)seria uma sobrecarga, que as coleções nunca chamam, porque elas enxergam os elementos comoObject. É exatamente o problema da sobrecarga decidida pelo tipo estático, visto em "Um Tipo, Várias Formas". O@Overridepega esse erro. - O teste de identidade é um atalho: um objeto é sempre igual a si mesmo, e não é preciso comparar campos.
- O
instanceofcom padrão faz duas coisas: devolve falso paranull, porquenull instanceof Qualqueré sempre falso, e para objetos de outros tipos. Se passar, a variáveloutrojá é umPonto. - Os campos são comparados. Para
double, usa-seDouble.compare, e não==, por causa de dois casos especiais do ponto flutuante de "Cada Valor em sua Caixa":
IO.println(0.0 == -0.0); // true
IO.println(Double.compare(0.0, -0.0) == 0); // false
double nan = 0.0 / 0;
IO.println(nan == nan); // false
IO.println(Double.compare(nan, nan) == 0); // true
Com ==, um NaN não é igual a si mesmo, e isso violaria a reflexividade. Double.compare trata NaN como igual a NaN e distingue 0.0 de -0.0, que é o comportamento coerente para o equals. Para campos int, long, char e boolean, o == basta. Para campos que são objetos, usa-se o equals deles, e, quando o campo pode ser null, Objects.equals(a, b), que trata o null com segurança.
Com isso:
Ponto a = new Ponto(1, 2);
Ponto b = new Ponto(1, 2);
IO.println(a == b); // false: são dois objetos
IO.println(a.equals(b)); // true: representam o mesmo ponto
hashCode: a outra metade
O contrato tem uma segunda parte, e é ela que costuma ser esquecida: se dois objetos são iguais pelo equals, eles devem ter o mesmo hashCode.
O hashCode devolve um inteiro calculado a partir do conteúdo do objeto. Estruturas de dados como o HashSet e o HashMap, que você vai conhecer a fundo em "Listas, Conjuntos e Mapas", usam esse número para decidir em que "gaveta" guardar cada objeto. Numa busca, elas calculam o hashCode do objeto procurado, vão direto à gaveta correspondente e só lá usam o equals. É isso que torna a busca quase instantânea, mesmo com milhões de elementos.
Veja o que acontece com uma versão do Ponto que tem equals, mas não hashCode:
HashSet<Ponto> visitados = new HashSet<>();
visitados.add(new Ponto(1, 2));
IO.println(visitados.contains(new Ponto(1, 2))); // false!
(O <Ponto> indica o tipo dos elementos do conjunto, um recurso explicado em "Escreva uma Vez para Todo Tipo".) O equals diz que os pontos são iguais, mas o conjunto não os encontra. Sem sobrescrita, o hashCode herdado é derivado da identidade do objeto, e dois objetos diferentes quase certamente têm códigos diferentes. O conjunto procura o segundo ponto na gaveta errada e nunca chega a chamar o equals. Nenhuma exceção, nenhum aviso: o programa simplesmente dá a resposta errada.
A implementação correta usa os mesmos campos que o equals:
@Override
public int hashCode() {
return Objects.hash(x, y);
}
Objects.hash combina os valores dos campos num único inteiro. Agora new Ponto(1, 2).hashCode() é igual para qualquer ponto com essas coordenadas, e o contains devolve true.
O contrato não exige o contrário: dois objetos diferentes podem ter o mesmo hashCode. É uma colisão, e é inevitável, porque há mais objetos possíveis do que inteiros. "Aa".hashCode() e "BB".hashCode() valem ambos 2112. Colisões só deixam as estruturas mais lentas, porque aí o equals desempata. Um bom hashCode espalha os objetos de forma que as colisões sejam raras, e Objects.hash faz isso razoavelmente bem, levando em conta a ordem dos campos: Objects.hash(1.0, 2.0) e Objects.hash(2.0, 1.0) dão valores diferentes.
A regra de ouro: sobrescreveu equals, sobrescreva hashCode, sempre, com os mesmos campos.
toString: completando o trio
O terceiro método de Object que toda classe de valor deveria sobrescrever é o toString, que você já conheceu em "Do Geral ao Específico":
@Override
public String toString() {
return "Ponto(" + x + ", " + y + ")";
}
Não faz parte do contrato de igualdade, mas completa o conjunto de uma classe que se comporta bem: iguais quando devem ser, encontráveis em coleções e legíveis em mensagens de erro, logs e depuração. Um Ponto@6d06d69c num log às três da manhã não ajuda ninguém.
Quando herança e igualdade se chocam
A simetria parece a propriedade mais fácil de cumprir, e é justamente a que a herança quebra com mais facilidade. Imagine um PontoColorido que estende Ponto (numa versão do Ponto sem o final) e acrescenta uma cor, com um equals que também compara a cor:
static class PontoColorido extends Ponto {
final String cor;
// ...
@Override
public boolean equals(Object o) {
if (!(o instanceof PontoColorido pc)) {
return false;
}
return super.equals(pc) && cor.equals(pc.cor);
}
}
Ponto p = new Ponto(1, 2);
PontoColorido pc = new PontoColorido(1, 2, "vermelho");
IO.println(p.equals(pc)); // true
IO.println(pc.equals(p)); // false
A simetria foi violada. O equals do Ponto aceita qualquer Ponto, inclusive um colorido, e compara só as coordenadas. O do PontoColorido recusa pontos sem cor. Uma coleção que contenha os dois pode se comportar de formas diferentes conforme a ordem em que os objetos foram inseridos.
Não existe solução perfeita para isso. É um resultado conhecido: não há como estender uma classe instanciável, acrescentar um campo que participa da igualdade e preservar o contrato do equals. As saídas são:
- Usar composição em vez de herança. O
PontoColoridotem umPontoe uma cor, em vez de ser umPonto. É a recomendação de "Do Geral ao Específico", agora com um motivo concreto. - Comparar com
getClass()em vez deinstanceof. ComgetClass() != o.getClass(), objetos de classes diferentes nunca são iguais. Preserva a simetria, mas umPontoColoridodeixa de ser igual a qualquerPonto, o que quebra a substituição de Liskov. - Declarar a classe de valor como
final, como fizemos com oPonto. Sem subclasses, o problema não existe.
Na prática, a terceira opção resolve a grande maioria dos casos. Classes que representam valores quase nunca precisam de subclasses.
Objetos mutáveis e coleções não combinam
O hashCode depende dos campos. O que acontece se um campo mudar enquanto o objeto está dentro de um HashSet?
HashSet<Caixa> conjunto = new HashSet<>();
Caixa caixa = new Caixa(1); // equals e hashCode baseados em "valor"
conjunto.add(caixa);
caixa.valor = 2;
IO.println(conjunto.contains(caixa)); // false
IO.println(conjunto.size()); // 1
O objeto está no conjunto, que tem tamanho 1, e mesmo assim o conjunto diz que ele não está lá. A caixa foi guardada na gaveta correspondente ao hashCode de quando o valor era 1. Depois da mudança, o hashCode é outro, e a busca vai à gaveta errada. O objeto ficou perdido dentro da coleção: não é encontrado e não pode ser removido pelo remove.
A lição reforça "O que Fica Dentro, o que Fica Fora": objetos usados como elementos de conjuntos ou chaves de mapas devem ser imutáveis, ou, pelo menos, os campos que participam do equals e do hashCode não devem mudar. É mais uma vantagem dos objetos imutáveis, como String, Integer e o nosso Ponto.
Identidade escondida: caches e strings
Algumas surpresas do == vêm da própria biblioteca, que reaproveita objetos por eficiência:
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
IO.println(a == b); // true
IO.println(c == d); // false
IO.println(c.equals(d)); // true
Integer é a classe que embrulha um int num objeto, e a conversão automática entre os dois (o autoboxing) é assunto de "Escreva uma Vez para Todo Tipo". O que importa aqui: a biblioteca mantém prontos os objetos Integer de −128 a 127, e os reaproveita. 127 vem do cache, e as duas variáveis apontam para o mesmo objeto. 128 está fora do cache, e cada atribuição cria um objeto novo. Um código que compara Integer com == funciona em todos os testes com números pequenos e falha em produção com o primeiro valor acima de 127.
Com String acontece algo parecido, e agora dá para explicar o "às vezes funciona" do exercício 2 de "Operadores, Texto e Conversa com o Usuário":
String s1 = "java";
String s2 = "java";
String s3 = new String("java");
String s4 = "ja" + "va";
String parte = "ja";
String s5 = parte + "va";
IO.println(s1 == s2); // true
IO.println(s1 == s3); // false
IO.println(s1 == s4); // true
IO.println(s1 == s5); // false
IO.println(s1 == s5.intern()); // true
Os literais de texto são guardados numa tabela da JVM (o string pool), e literais iguais apontam para o mesmo objeto. "ja" + "va" é calculado pelo compilador, porque os dois lados são constantes, e vira o literal "java". parte + "va" é calculado na execução e produz um objeto novo, assim como new String(...). O método intern() devolve o objeto da tabela com o mesmo conteúdo. Nada disso é algo em que se deva confiar. É exatamente por isso que a regra é comparar objetos sempre com equals. O == só é correto para tipos primitivos, para enums (que você vai ver na próxima aula) e quando a pergunta é mesmo de identidade.
Toda classe deve sobrescrever equals?
Não. A decisão depende do que a classe representa.
Objetos de valor, definidos pelo conteúdo, como Ponto, uma quantia de dinheiro, uma data ou um CPF, devem sobrescrever equals, hashCode e toString, e de preferência ser imutáveis.
Entidades, que têm identidade própria e estado que muda com o tempo, geralmente não devem. Duas contas bancárias com o mesmo titular e o mesmo saldo não são a mesma conta. E a mesma conta continua sendo ela mesma depois de um saque, quando o saldo muda. Para a ContaBancaria, o equals herdado, de identidade, está correto. Se for preciso comparar contas guardadas e recarregadas de um banco de dados, a comparação é pelo identificador, o número da conta, e nunca pelo saldo. Essa distinção entre entidades e objetos de valor volta com força em "Objetos no Banco", na trilha de persistência.
Escrever equals, hashCode e toString corretamente para cada classe de valor é trabalhoso e repetitivo: são dezenas de linhas para dizer algo óbvio, que o Ponto é igual a outro quando as coordenadas coincidem. E cada linha escrita à mão é uma chance de errar, como esquecer um campo no hashCode. O Java moderno tem uma resposta para isso, capaz de reduzir a classe Ponto inteira a uma única linha.
Fontes e leituras recomendadas
- Documentação de
Object.equalseObject.hashCode(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#equals(java.lang.Object). O texto oficial do contrato, com cada propriedade enunciada. - Joshua Bloch, Effective Java, 3ª edição, itens 10, 11 e 12: "Obey the general contract when overriding equals", "Always override hashCode when you override equals" e "Always override toString". O exemplo do ponto colorido desta aula vem do item 10, onde é discutido em profundidade.
- Documentação da classe
Objects: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Objects.html. Os utilitáriosequals,hash,hashCodeerequireNonNull, que simplificam essas implementações. - Martin Odersky, Lex Spoon e Bill Venners, "How to Write an Equality Method in Java" (2009): https://www.artima.com/articles/how-to-write-an-equality-method-in-java. Um artigo detalhado sobre as quatro armadilhas mais comuns do
equals, com a questão da herança tratada com cuidado. - Eric Evans, Domain-Driven Design (Alta Books), capítulo 5: a distinção clássica entre entidades e objetos de valor, que fundamenta a decisão de quando sobrescrever
equals.
Exercícios
Exercício 1
Sem rodar, preveja cada linha e explique:
Integer x = 100, y = 100, z = 1000, w = 1000;
String a = "curso", b = "cur" + "so", c = new String("curso");
IO.println(x == y);
IO.println(z == w);
IO.println(a == b);
IO.println(a == c);
IO.println(a.equals(c));
Ver resposta
✓ Resposta: true, false, true, false, true. x e y valem 100, dentro da faixa de −128 a 127 que a biblioteca mantém em cache, e apontam para o mesmo objeto. z e w valem 1000, fora do cache, e cada um é um objeto novo. "cur" + "so" é uma concatenação de constantes, calculada pelo compilador, e resulta no mesmo literal da tabela de strings que a. new String("curso") sempre cria um objeto novo. O equals compara conteúdo e dá true. O exercício mostra por que o == com objetos é traiçoeiro: os resultados dependem de detalhes de implementação, como o tamanho do cache e as otimizações do compilador, que não fazem parte da lógica do programa.
Exercício 2
Escreva uma classe imutável Dinheiro com os campos long centavos e String moeda (como "BRL" ou "USD"), com validação no construtor (a moeda não pode ser nula nem vazia) e com equals, hashCode e toString corretos. Por que guardar centavos num long em vez de reais num double?
Ver resposta
✓ Resposta:
import java.util.Objects;
public final class Dinheiro {
private final long centavos;
private final String moeda;
public Dinheiro(long centavos, String moeda) {
if (moeda == null || moeda.isBlank()) {
throw new IllegalArgumentException("Moeda é obrigatória");
}
this.centavos = centavos;
this.moeda = moeda;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Dinheiro outro)) {
return false;
}
return centavos == outro.centavos && moeda.equals(outro.moeda);
}
@Override
public int hashCode() {
return Objects.hash(centavos, moeda);
}
@Override
public String toString() {
return "%s %d.%02d".formatted(moeda, centavos / 100, Math.abs(centavos % 100));
}
}
new Dinheiro(1990, "BRL") é igual a outro new Dinheiro(1990, "BRL"), diferente de new Dinheiro(1990, "USD"), e aparece como "BRL 19.90". Como moeda nunca é null, graças à validação, o equals pode chamar moeda.equals diretamente. Centavos num long evitam a imprecisão do double vista em "Cada Valor em sua Caixa": somar e comparar inteiros é exato, e 0,10 + 0,20 dá 30 centavos, e não 0,30000000000000004. A classe ainda é simples e não lida com valores negativos na formatação. A solução completa para dinheiro, com BigDecimal, está em "Tempo, Dinheiro e Padrões".
Exercício 3
Explique por que o PontoColorido da aula quebra a simetria do equals e por que declarar Ponto como final resolve o problema. Qual é o custo dessa solução?
Ver resposta
✓ Resposta: O equals do Ponto usa instanceof Ponto, que aceita qualquer subclasse, e compara só as coordenadas. Assim, p.equals(pc) é verdadeiro. O equals do PontoColorido exige que o outro objeto também seja colorido, então pc.equals(p) é falso. A mesma pergunta, feita nas duas direções, recebe respostas diferentes. Com Ponto declarado final, não pode haver subclasses, e todo objeto que passa no instanceof Ponto é exatamente um Ponto, comparado sempre pelos mesmos campos. O custo é que não se pode mais estender Ponto. Para um ponto colorido, usa-se composição: uma classe PontoColorido com um campo Ponto e um campo cor, que não é um Ponto e não se compara com pontos simples. Para classes de valor, o custo é baixo, porque raramente se quer estendê-las.
Exercício 4
No exemplo da Caixa mutável dentro do HashSet, o objeto "sumiu" da coleção depois de mudar de valor. Explique o mecanismo e diga duas formas de evitar o problema.
Ver resposta
✓ Resposta: O HashSet guarda cada objeto numa posição determinada pelo hashCode no momento da inserção. Quando o campo valor muda, o hashCode passa a ser outro, mas o objeto continua guardado na posição antiga. Um contains ou um remove calcula o hashCode atual e procura na posição nova, onde não há nada, sem nunca chegar a chamar o equals com o objeto certo. O objeto continua ocupando espaço, aparece numa iteração, mas não pode ser encontrado nem removido pelas operações normais. Primeira forma de evitar: tornar a classe imutável, para que o hashCode nunca mude. Segunda: se o objeto precisa ser mutável, basear equals e hashCode só em campos que nunca mudam, como um identificador final. E, se for inevitável alterar um campo relevante, remover o objeto da coleção antes da mudança e reinseri-lo depois.
Exercício 5
Um colega sugere sobrescrever equals e hashCode na ContaBancaria, comparando titular e saldo. Explique por que isso é um erro e o que seria razoável fazer, se alguma comparação fosse necessária.
Ver resposta
✓ Resposta: A conta é uma entidade: tem identidade própria, que persiste enquanto o estado muda. Comparar por titular e saldo cria dois absurdos. Duas contas diferentes da mesma pessoa, que por acaso tenham o mesmo saldo, passariam a ser "iguais", e uma coleção de contas trataria uma delas como duplicata e a descartaria. E como o saldo muda a cada operação, o hashCode mudaria junto, e a conta se perderia dentro de qualquer HashSet, como a Caixa do exercício anterior. O equals herdado, de identidade, é o correto para uma entidade enquanto ela vive na memória. Se for necessário comparar contas carregadas de fontes diferentes, como duas leituras do mesmo registro num banco de dados, a igualdade deve se basear só no identificador imutável, o numero, que é final e único.