equals()
e hashCode()
. Não é a primeira vez que os encontramos: o curso CodeGym começa com uma breve aula sobre equals()
— leia se você esqueceu ou não viu antes... 
==
operador, porque ==
compara referências. Aqui está o nosso exemplo com carros de uma lição recente:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
Saída do console:
false
Parece que criamos dois Car
objetos idênticos: os valores dos campos correspondentes dos dois objetos carro são os mesmos, mas o resultado da comparação ainda é falso. Já sabemos o motivo: as referências car1
e car2
apontam para endereços de memória diferentes, portanto não são iguais. Mas ainda queremos comparar os dois objetos, não duas referências. A melhor solução para comparar objetos é o equals()
método.
método equals()
Você deve se lembrar que não criamos esse método do zero, mas o sobrescrevemos: oequals()
método é definido na Object
classe. Dito isto, em sua forma usual, é de pouca utilidade:
public boolean equals(Object obj) {
return (this == obj);
}
É assim que o equals()
método é definido na Object
classe. Esta é uma comparação de referências mais uma vez. Por que eles fizeram assim? Bem, como os criadores da linguagem sabem quais objetos em seu programa são considerados iguais e quais não são? :) Este é o ponto principal do equals()
método — o criador de uma classe é quem determina quais características são usadas ao verificar a igualdade dos objetos da classe. Então você substitui o equals()
método em sua classe. Se você não entender bem o significado de "determina quais características", vamos considerar um exemplo. Aqui está uma classe simples representando um homem: Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
// Getters, setters, etc.
}
Suponha que estamos escrevendo um programa que precisa determinar se duas pessoas são gêmeas idênticas ou simplesmente parecidas. Temos cinco características: tamanho do nariz, cor dos olhos, estilo do cabelo, presença de cicatrizes e resultados de testes de DNA (para simplificar, representamos isso como um código inteiro). Quais dessas características você acha que permitiriam ao nosso programa identificar gêmeos idênticos? 
equals()
método? Precisamos substituí-lo noMan
classe, tendo em conta os requisitos do nosso programa. O método deve comparar o int dnaCode
campo dos dois objetos. Se eles são iguais, então os objetos são iguais.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
É realmente assim tão simples? Na verdade. Nós esquecemos algo. Para nossos objetos, identificamos apenas um campo relevante para estabelecer a igualdade de objetos: dnaCode
. Agora imagine que não temos 1, mas 50 campos relevantes. E se todos os 50 campos de dois objetos forem iguais, então os objetos serão iguais. Tal cenário também é possível. O principal problema é que estabelecer a igualdade comparando 50 campos é um processo demorado e com uso intensivo de recursos. Agora imagine que além da nossa Man
classe, temos uma Woman
classe com exatamente os mesmos campos que existem em Man
. Se outro programador usar nossas classes, ele ou ela poderá facilmente escrever um código como este:
public static void main(String[] args) {
Man man = new Man(........); // A bunch of parameters in the constructor
Woman woman = new Woman(.........); // The same bunch of parameters.
System.out.println(man.equals(woman));
}
Nesse caso, verificar os valores dos campos não faz sentido: podemos ver prontamente que temos objetos de duas classes diferentes, portanto não há como eles serem iguais! Isso significa que devemos adicionar uma verificação ao equals()
método, comparando as classes dos objetos comparados. Que bom que pensamos nisso!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Mas talvez tenhamos esquecido outra coisa? Hmm... No mínimo, devemos verificar se não estamos comparando um objeto consigo mesmo! Se as referências A e B apontam para o mesmo endereço de memória, então são o mesmo objeto e não precisamos perder tempo comparando 50 campos.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Também não custa nada adicionar uma verificação para null
: nenhum objeto pode ser igual a null
. Portanto, se o parâmetro do método for nulo, não há sentido em verificações adicionais. Com tudo isso em mente, nosso equals()
método para a Man
classe fica assim:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Realizamos todas as verificações iniciais mencionadas acima. No final do dia, se:
- estamos comparando dois objetos da mesma classe
- e os objetos comparados não são o mesmo objeto
- e o objeto passado não é
null
dnaCode
campos dos dois objetos. Ao substituir o equals()
método, certifique-se de observar estes requisitos:
-
Reflexividade.
Quando o
equals()
método é usado para comparar qualquer objeto consigo mesmo, ele deve retornar true.
Já cumprimos esse requisito. Nosso método inclui:if (this == o) return true;
-
Simetria.
Se
a.equals(b) == true
, entãob.equals(a)
deve retornartrue
.
Nosso método também atende a esse requisito. -
Transitividade.
Se dois objetos são iguais a algum terceiro objeto, então eles devem ser iguais entre si.
Sea.equals(b) == true
ea.equals(c) == true
, entãob.equals(c)
também deve retornar verdadeiro. -
Persistência.
O resultado de
equals()
deve mudar apenas quando os campos envolvidos forem alterados. Se os dados dos dois objetos não mudarem, o resultado deequals()
deve ser sempre o mesmo. -
Desigualdade com
null
.Para qualquer objeto,
a.equals(null)
deve retornar false
Isso não é apenas um conjunto de algumas "recomendações úteis", mas sim um contrato estrito , definido na documentação do Oracle
método hashCode()
Agora vamos falar sobre ohashCode()
método. Por que é necessário? Exatamente para o mesmo propósito - comparar objetos. Mas já temos equals()
! Por que outro método? A resposta é simples: melhorar o desempenho. Uma função hash, representada em Java usando o hashCode()
método, retorna um valor numérico de comprimento fixo para qualquer objeto. Em Java, o hashCode()
método retorna um número de 32 bits ( int
) para qualquer objeto. Comparar dois números é muito mais rápido do que comparar dois objetos usando o equals()
método, especialmente se esse método considerar muitos campos. Se nosso programa comparar objetos, isso é muito mais simples de fazer usando um código hash. Somente se os objetos forem iguais com base no hashCode()
método, a comparação prossegue para oequals()
método. A propósito, é assim que as estruturas de dados baseadas em hash funcionam, por exemplo, o familiar HashMap
! O hashCode()
método, como o equals()
método, é substituído pelo desenvolvedor. E assim como equals()
, o hashCode()
método tem requisitos oficiais descritos na documentação do Oracle:
-
Se dois objetos forem iguais (ou seja, o
equals()
método retorna true), eles devem ter o mesmo código hash.Caso contrário, nossos métodos não teriam sentido. Como mencionamos acima, uma
hashCode()
verificação deve ser feita primeiro para melhorar o desempenho. Se os códigos hash fossem diferentes, a verificação retornaria falso, mesmo que os objetos sejam realmente iguais de acordo com a definição doequals()
método. -
Se o
hashCode()
método for chamado várias vezes no mesmo objeto, ele deverá retornar o mesmo número todas as vezes. -
A regra 1 não funciona na direção oposta. Dois objetos diferentes podem ter o mesmo código hash.
hashCode()
método retorna um arquivo int
. An int
é um número de 32 bits. Tem um intervalo limitado de valores: de -2.147.483.648 a +2.147.483.647. Em outras palavras, existem pouco mais de 4 bilhões de valores possíveis para um arquivo int
. Agora imagine que você está criando um programa para armazenar dados sobre todas as pessoas que vivem na Terra. Cada pessoa corresponderá ao seu próprio Person
objeto (semelhante à Man
classe). Existem cerca de 7,5 bilhões de pessoas vivendo no planeta. Em outras palavras, não importa quão inteligente seja o algoritmo que escrevemos para converterPerson
objetos para um int, simplesmente não temos números possíveis suficientes. Temos apenas 4,5 bilhões de valores int possíveis, mas há muito mais pessoas do que isso. Isso significa que não importa o quanto tentemos, algumas pessoas diferentes terão os mesmos códigos hash. Quando isso acontece (códigos de hash coincidem para dois objetos diferentes), chamamos de colisão. Ao substituir o hashCode()
método, um dos objetivos do programador é minimizar o número potencial de colisões. Contabilizando todas essas regras, como ficará o hashCode()
método na Person
classe? Assim:
@Override
public int hashCode() {
return dnaCode;
}
Surpreso? :) Se você observar os requisitos, verá que cumprimos todos eles. Objetos para os quais nosso equals()
método retorna true também serão iguais de acordo com hashCode()
. Se nossos dois Person
objetos forem iguais em equals
(ou seja, eles tiverem o mesmo dnaCode
), nosso método retornará o mesmo número. Vamos considerar um exemplo mais difícil. Suponha que nosso programa deva selecionar carros de luxo para colecionadores. Colecionar pode ser um hobby complexo com muitas peculiaridades. Um determinado carro de 1963 pode custar 100 vezes mais do que um carro de 1964. Um carro vermelho de 1970 pode custar 100 vezes mais que um carro azul da mesma marca do mesmo ano. 
Person
classe, descartamos a maioria dos campos (ou seja, características humanas) como insignificantes e usamos apenas osdnaCode
campo nas comparações. Agora estamos trabalhando em um reino muito idiossincrático, no qual não há detalhes insignificantes! Aqui está nossa LuxuryAuto
aula:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
// ...getters, setters, etc.
}
Agora devemos considerar todos os campos em nossas comparações. Qualquer erro pode custar a um cliente centenas de milhares de dólares, então seria melhor ser excessivamente seguro:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
Em nosso equals()
método, não esquecemos todas as verificações sobre as quais falamos anteriormente. Mas agora comparamos cada um dos três campos de nossos objetos. Para este programa, precisamos de igualdade absoluta, ou seja, igualdade de cada campo. E sobre hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
O model
campo em nossa classe é uma String. Isso é conveniente porque a String
classe já substitui o hashCode()
método. Calculamos o model
código hash do campo e, em seguida, adicionamos a soma dos outros dois campos numéricos a ele. Os desenvolvedores Java têm um truque simples que usam para reduzir o número de colisões: ao calcular um código hash, multiplique o resultado intermediário por um primo ímpar. O número mais comumente usado é 29 ou 31. Não vamos nos aprofundar nas sutilezas matemáticas agora, mas no futuro, lembre-se de que multiplicar os resultados intermediários por um número ímpar suficientemente grande ajuda a "espalhar" os resultados da função hash e, conseqüentemente, reduza o número de objetos com o mesmo código hash. Para o nosso hashCode()
método no LuxuryAuto, ficaria assim:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Você pode ler mais sobre todas as complexidades desse mecanismo neste post no StackOverflow , bem como no livro Effective Java de Joshua Bloch. Finalmente, mais um ponto importante que vale a pena mencionar. Cada vez que sobrescrevemos o método equals()
and hashCode()
, selecionamos certos campos de instância que são levados em consideração nesses métodos. Esses métodos consideram os mesmos campos. Mas podemos considerar campos diferentes em equals()
e hashCode()
? Tecnicamente, podemos. Mas esta é uma má ideia, e aqui está o porquê:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Aqui estão nossos métodos equals()
e hashCode()
para a LuxuryAuto
classe. O hashCode()
método permaneceu inalterado, mas removemos o model
campo do equals()
método. O modelo não é mais uma característica utilizada quando o equals()
método compara dois objetos. Mas ao calcular o código hash, esse campo ainda é levado em consideração. O que obtemos como resultado? Vamos criar dois carros e descobrir!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Are these two objects equal to each other?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("What are their hash codes?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Are these two objects equal to each other?
true
What are their hash codes?
-1372326051
1668702472
Erro! Ao usar campos diferentes para os métodos equals()
e hashCode()
, violamos os contratos que foram estabelecidos para eles! Dois objetos iguais de acordo com o equals()
método devem ter o mesmo código hash. Recebemos valores diferentes para eles. Esses erros podem levar a consequências absolutamente inacreditáveis, especialmente ao trabalhar com coleções que usam um hash. Como resultado, ao substituir equals()
e hashCode()
, você deve considerar os mesmos campos. Esta lição foi bastante longa, mas você aprendeu muito hoje! :) Agora é hora de voltar a resolver as tarefas!
GO TO FULL VERSION