Por que substituir os métodos equals e hashcode em Java?
Fonte:
Medium Este artigo se concentra em dois métodos intimamente relacionados: equals() e hashcode() . Você aprenderá como eles interagem entre si e como substituí-los corretamente.
Por que substituímos o método equals()?
Em Java, não podemos sobrecarregar o comportamento de operadores como
== ,
+= ,
-+ . Eles funcionam de acordo com um determinado processo. Por exemplo, considere a operação do operador
== .
Como funciona o operador ==?
Ele verifica se as duas referências comparadas apontam para a mesma instância na memória. O operador
== só será avaliado como verdadeiro se as duas referências representarem a mesma instância na memória. Vamos dar uma olhada no código de exemplo:
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
}
Digamos que em seu programa você criou dois objetos
Person em locais diferentes e deseja compará-los.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println( person1 == person2 ); --> will print false!
Do ponto de vista empresarial, os dois parecem iguais, certo? Mas para a JVM eles não são iguais. Como ambos são criados usando a palavra-chave
new , essas instâncias estão localizadas em segmentos de memória diferentes. Portanto, o operador
== retornará
false . Mas se não podemos substituir o operador
== , então como podemos dizer à JVM que queremos que esses dois objetos sejam tratados da mesma forma?
É aqui que o método .equals() entra em ação . Você pode substituir
equals() para verificar se alguns objetos possuem os mesmos valores para determinados campos, a fim de considerá-los iguais. Você pode escolher quais campos comparar. Se dissermos que dois objetos
Person serão iguais apenas se tiverem a mesma idade e o mesmo nome, então o IDE irá gerar algo assim para criar automaticamente
equals() :
@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 &&
name.equals(person.name);
}
Voltemos ao nosso exemplo anterior.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
Sim, não podemos sobrecarregar o operador
== para comparar objetos da maneira que desejamos, mas Java nos oferece outra maneira - o método
equals() , que podemos substituir como desejarmos.
Tenha em mente que se não fornecermos nossa versão personalizada de .equals() (também conhecida como substituição) em nossa classe, então o .equals() predefinido da classe Object e o operador == se comportarão da mesma forma. O método
equals() padrão , herdado de
Object , verificará se ambas as instâncias comparadas são iguais na memória!
Por que estamos substituindo o método hashCode()?
Algumas estruturas de dados em Java, como
HashSet e
HashMap , armazenam seus elementos com base em uma função hash aplicada a esses elementos. A função hash é
hashCode() . Se tivermos a opção de substituir
o método .equals() , também deveremos ter a opção de substituir
o método hashCode() . Há uma razão para isso. Afinal, a implementação padrão
de hashCode() , herdada de
Object , considera todos os objetos na memória como únicos! Mas vamos voltar a essas estruturas de dados hash. Existe uma regra para essas estruturas de dados.
Um HashSet não pode conter valores duplicados e um HashMap não pode conter chaves duplicadas. Um HashSet é implementado usando
um HashMap de forma que cada valor
do HashSet seja armazenado como uma chave no
HashMap . Como funciona
o HashMap ?
HashMap é um array nativo com vários segmentos. Cada segmento possui uma lista vinculada (
linkedList ). Esta lista vinculada armazena nossas chaves.
HashMap encontra o linkedList correto para cada chave usando o método
hashCode() e, em seguida, itera por todos os elementos desse
linkedList e aplica o método
equals() a cada um desses elementos para verificar se esse elemento está contido lá.
Chaves duplicadas não são permitidas. Quando colocamos algo dentro de
um HashMap , a chave é armazenada em uma dessas listas vinculadas. A lista vinculada em que esta chave será armazenada é mostrada pelo resultado do método
hashCode() para essa chave. Ou seja, se
key1.hashCode() resultar em 4, então essa
key1 será armazenada no 4º segmento do array no
LinkedList ali existente . Por padrão, o método
hashCode() retorna resultados diferentes para cada instância. Se tivermos um
equals() padrão que se comporte como
== , tratando todas as instâncias na memória como objetos diferentes, então não haverá problema. Como você deve se lembrar, em nosso exemplo anterior dissemos que queremos que as instâncias
Person sejam consideradas iguais se suas idades e nomes forem iguais.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) ); --> will print true!
Agora vamos criar um mapa para armazenar essas instâncias como chaves com uma string específica como par de valores.
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
Na classe
Person , não substituímos o método
hashCode , mas temos um método
equals substituído . Como o
hashCode padrão fornece resultados diferentes para diferentes instâncias Java
de person1.hashCode() e
person2.hashCode() , há grandes chances de obter resultados diferentes. Nosso mapa pode terminar com diferentes
pessoas em diferentes listas vinculadas.
Isso vai contra a lógica
do HashMap .
Afinal, um HashMap não pode ter várias chaves idênticas! A questão é que o
hashCode() padrão herdado da classe
Object não é suficiente. Mesmo depois de substituirmos o método
equals() da classe
Person . É por isso que temos que substituir o método
hashCode() depois de substituir o método
equals . Agora vamos consertar isso. Precisamos substituir nosso método
hashCode() para que ele leve em consideração os mesmos campos de
equals() , ou seja,
age e
name .
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
@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 &&
name.equals(person.name);
}
@Override
public int hashCode() {
int prime = 31;
return prime*Objects.hash(name, age);
}
No método
hashCode() usamos um valor simples (você pode usar qualquer outro valor). No entanto, sugere-se a utilização de números primos para criar menos problemas.
Vamos tentar armazenar essas chaves em nosso HashMap novamente :
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode() e
person2.hashCode() serão iguais. Digamos que sejam 0.
O HashMap irá para o segmento 0 e nele
o LinkedList salvará
person1 como uma chave com o valor “1”. No segundo caso, quando
o HashMap for novamente ao bucket 0 para armazenar a chave
person2 com o valor “2”, ele verá que lá já existe outra chave igual a ela. Dessa forma, ele substituirá a chave anterior. E apenas a chave
person2 existirá em nosso
HashMap . Foi assim que aprendemos como funciona a regra
HashMap , que afirma que você não pode usar várias chaves idênticas!
No entanto, lembre-se de que instâncias desiguais podem ter o mesmo código hash e instâncias iguais devem retornar o mesmo código hash.
GO TO FULL VERSION