Desafiadores de Java # 4: Comparando objetos com equals () e hashCode ()

Desafiadores de Java # 4: Comparando objetos com equals () e hashCode ()


Antecipando o lançamento de um novo thread no curso "Java Developer", continuamos a traduzir uma série de artigos sobre o Java Challengers, cujas partes anteriores podem ser lidas nos links abaixo:



Vamos lá!


Neste artigo, você aprenderá como os métodos equals() e hashCode() estão relacionados e como eles são usados ​​na comparação de objetos.


equals-hashcode


Sem usar equals() e hashCode() para comparar o estado de dois objetos, precisamos escrever muitas comparações " if " comparando cada campo do objeto. Essa abordagem torna o código confuso e difícil de ler. Trabalhando juntos, esses dois métodos ajudam a criar código mais flexível e consistente.


O código fonte do artigo está aqui .


Substituindo equals () e hashCode ()


A substituição de método é uma técnica pela qual o comportamento de uma classe ou interface pai é reescrito (redefinido) em uma subclasse (consulte Java Challengers # 3: Polymorphism and Inheritance , Eng. ). Em Java, todo objeto tem métodos equals() e hashCode() e, para funcionar corretamente, eles devem ser substituídos.


Para entender como a redefinição de equals() e hashCode() funciona, vamos examinar sua implementação nas classes base Java. A seguir, é apresentado o método equals() da classe Object . O método verifica se a instância atual corresponde ao objeto obj passado.


 public boolean equals(Object obj) { return (this == obj); } 

Agora, vejamos o método hashCode() na classe Object .


 @HotSpotIntrinsicCandidate public native int hashCode(); 

Isso é nativo - um método escrito em outro idioma, como C, e retorna algum código numérico associado ao endereço de memória do objeto. (Se você não estiver escrevendo código JDK, não é importante saber exatamente como esse método funciona.)
Nota do tradutor: o valor associado ao endereço não está completamente correto ( graças a vladimir_dolzhenko ). A JVM do HotSpot usa números pseudo-aleatórios por padrão. A descrição da implementação de hashCode () para HotSpot está aqui e aqui .


Se os métodos equals() e hashCode() não forem substituídos, os métodos da classe Object descritos acima serão chamados. Nesse caso, os métodos não cumprem o objetivo real de equals() e hashCode() , que é verificar se os objetos têm o mesmo estado.


Normalmente, a substituição de equals() também substitui o hashCode() .


Comparando objetos com equals ()


O método equals() é usado para comparar objetos. Para determinar se os objetos são idênticos ou não, equals() compara os valores do campo do objeto:


 public class EqualsAndHashCodeExample { public static void main(String... args){ System.out.println(new Simpson("Homer", 35, 120) .equals(new Simpson("Homer",35,120))); System.out.println(new Simpson("Bart", 10, 120) .equals(new Simpson("El Barto", 10, 45))); System.out.println(new Simpson("Lisa", 54, 60) .equals(new Object())); } static class Simpson { private String name; private int age; private int weight; public Simpson(String name, int age, int weight) { this.name = name; this.age = age; this.weight = weight; } @Override public boolean equals(Object o) { // 1 if (this == o) { return true; } // 2 if (o == null || getClass() != o.getClass()) { return false; } // 3 Simpson simpson = (Simpson) o; return age == simpson.age && weight == simpson.weight && name.equals(simpson.name); } } } 

Vejamos o método equals() . A primeira comparação compara a instância atual this com o passado. Se for o mesmo objeto, então equals() retornará true .


A segunda comparação verifica se o objeto passado é null e que tipo é. Se o objeto transferido for de um tipo diferente, os objetos não serão iguais.


Finalmente, equals() compara os campos dos objetos. Se dois objetos tiverem os mesmos valores de campo, os objetos serão os mesmos.


Análise de opções para comparar objetos


Agora vamos ver as opções para comparar objetos no método main() . Primeiro, comparamos dois objetos Simpson :


 System.out.println( new Simpson("Homer", 35, 120).equals( new Simpson("Homer", 35, 120))); 

Os campos desses objetos têm os mesmos valores, portanto o resultado será true .


Em seguida, compare novamente os dois objetos Simpson :


 System.out.println( new Simpson("Bart", 10, 45).equals( new Simpson("El Barto", 10, 45))); 

Os objetos aqui são semelhantes, mas os significados dos nomes são diferentes: Bart e El Barto . Portanto, o resultado será false .


Por fim, vamos comparar o objeto Simpson e a instância da classe Object :


 System.out.println( new Simpson("Lisa", 54, 60).equals( new Object())); 

Nesse caso, o resultado será false , pois os tipos de objetos são diferentes.


igual a () versus ==


À primeira vista, parece que o operador == e o método equals() fazem a mesma coisa, mas, de fato, eles funcionam de maneira diferente. O operador == compara se dois links apontam para o mesmo objeto. Por exemplo:


 Simpson homer = new Simpson("Homer", 35, 120); Simpson homer2 = new Simpson("Homer", 35, 120); System.out.println(homer == homer2); 

Criamos duas instâncias diferentes do Simpson usando o new operador. Portanto, as variáveis homer e homer2 para diferentes objetos no heap . Assim, como resultado, ficamos false .


No exemplo a seguir, usamos o método equals() substituído:


 System.out.println(homer.equals(homer2)); 

Nesse caso, os campos serão comparados. Como os dois objetos Simpson têm os mesmos valores de campo, o resultado será true .


Identificação de objetos com hashCode ()


Para otimizar o desempenho ao comparar objetos, o hashCode() é usado. O hashCode() retorna um identificador exclusivo para cada objeto, o que simplifica a comparação dos estados do objeto.


Se o código hash de um objeto não corresponder ao código hash de outro objeto, você poderá omitir o método equals() : você apenas sabe que os dois objetos não correspondem. Por outro lado, se o código de hash for o mesmo, será necessário executar o método equals() para determinar se os valores do campo correspondem.


Considere um exemplo prático com hashCode() .


 public class HashcodeConcept { public static void main(String... args) { Simpson homer = new Simpson(1, "Homer"); Simpson bart = new Simpson(2, "Homer"); boolean isHashcodeEquals = homer.hashCode() == bart.hashCode(); if (isHashcodeEquals) { System.out.println("   equals."); } else { System.out.println("    equals, .. " + " ,  ,     ."); } } static class Simpson { int id; String name; public Simpson(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Simpson simpson = (Simpson) o; return id == simpson.id && name.equals(simpson.name); } @Override public int hashCode() { return id; } } } 

O hashCode() , que sempre retorna o mesmo valor, é válido, mas não eficiente. Nesse caso, a comparação sempre retornará true , portanto o método equals() sempre será executado. Nesse caso, não há melhoria de desempenho.


Usando equals () e hashCode () com coleções


As classes que implementam a interface Set (set) devem impedir que elementos duplicados sejam adicionados. Abaixo estão algumas classes que implementam a interface Set :



Apenas elementos exclusivos podem ser adicionados ao Set . Portanto, se você deseja adicionar um elemento, por exemplo, a um HashSet , deve primeiro usar os métodos equals() e hashCode() para garantir que esse elemento seja exclusivo. Se os métodos equals() e hashCode() não foram substituídos, você corre o risco de inserir valores duplicados.


Vejamos a parte de implementação do método add() em um HashSet :


 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; 

Antes de adicionar um novo item, o HashSet verifica se o item existe nesta coleção. Se o objeto corresponder, o novo elemento não será inserido.


Os métodos equals() e hashCode() são usados ​​não apenas em Set . Esses métodos também são necessários para HashMap , Hashtable e LinkedHashMap . Como regra, se você vir uma coleção com o prefixo "Hash" , pode ter certeza de que, para sua operação correta, é necessária uma substituição dos métodos hashCode() e equals() .


Recomendações para usar equals () e hashCode ()


Execute o método equals() apenas para objetos com o mesmo código de hash. Não execute equals() se o código hash for diferente.


Tabela 1. Comparação de código hash


Se a comparação hashCode () ...Isso ...
retorna trueexecutar equals()
retorna falsenão execute equals()

Esse princípio é usado principalmente nas coleções Set ou Hash por motivos de desempenho.


Regras de comparação de objetos


Quando a comparação hashCode() retorna false , o método equals() também deve retornar false . Se o código hash for diferente, os objetos definitivamente não serão iguais.


Tabela 2. Comparação de objetos com hashCode ()


Quando a comparação hashCode() retornar ...O método equals() deve retornar ...
truetrue ou false
falsefalse

Quando o método equals() retorna true , isso significa que os objetos são iguais em todos os valores e atributos . Nesse caso, a comparação do código de hash também deve ser verdadeira.


Tabela 3. Comparação de Objetos com Equals ()


Quando o método equals() retorna ...O hashCode() deve retornar ...
truetrue
falsetrue ou false

Resolva o problema em equals () e hashCode ()


É hora de testar seu conhecimento dos métodos equals() e hashCode() . A tarefa é descobrir o resultado de vários equals() e o tamanho total da coleção Set .


Para começar, estude cuidadosamente o seguinte código:


 public class EqualsHashCodeChallenge { public static void main(String... args) { System.out.println(new Simpson("Bart").equals(new Simpson("Bart"))); Simpson overriddenHomer = new Simpson("Homer") { public int hashCode() { return (43 + 777) + 1; } }; System.out.println(new Simpson("Homer").equals(overriddenHomer)); Set set = new HashSet(Set.of(new Simpson("Homer"), new Simpson("Marge"))); set.add(new Simpson("Homer")); set.add(overriddenHomer); System.out.println(set.size()); } static class Simpson { String name; Simpson(String name) { this.name = name; } @Override public boolean equals(Object obj) { Simpson otherSimpson = (Simpson) obj; return this.name.equals(otherSimpson.name) && this.hashCode() == otherSimpson.hashCode(); } @Override public int hashCode() { return (43 + 777); } } } 

Primeiro, analise o código, pense sobre qual será o resultado. E só então execute o código. O objetivo é melhorar suas habilidades de análise de código e aprender conceitos básicos de Java para que você possa melhorar seu código.


Qual será o resultado?


 A) true true 4 B) true false 3 C) true false 2 D) false true 3 

O que aconteceu Compreendendo equals () e hashCode ()


Na primeira comparação, o resultado de equals() é true , pois os estados dos objetos são os mesmos e o hashCode() retorna o mesmo valor para os dois objetos.


Na segunda comparação, o hashCode() foi substituído pela variável hashCode() . Para os dois objetos Simpson , o nome é "Homer" , mas para overriddenHomer o hashCode() retorna um valor diferente. Nesse caso, o resultado do método equals() será false , pois contém uma comparação com o código hash.


Você deve ter percebido que haverá três objetos Simpson na coleção. Vamos fazer isso.


O primeiro objeto no conjunto será inserido como de costume:


 new Simpson("Homer"); //  

O objeto a seguir também será inserido da maneira usual, porque contém um valor diferente do objeto anterior:


 new Simpson("Marge"); //  

Finalmente, o próximo objeto Simpson tem o mesmo valor de nome que o primeiro objeto. Nesse caso, o objeto não será inserido:


 set.add(new Simpson("Homer")); //   

Como sabemos, o objeto overridenHomer usa um valor de hash diferente, diferente de uma instância regular de Simpson("Homer") . Por esse motivo, este item será inserido na coleção:


 set.add(overriddenHomer); //  

A resposta


A resposta correta é B. A conclusão será:


 true false 3 

Erros comuns com equals () e hashCode ()


  • Falta de substituição de hashCode() junto com substituição de equals() ou vice-versa.
  • Falta de substituição de equals() e hashCode() ao usar coleções de hash como HashSet .
  • Retornando um valor constante no método hashCode() vez de retornar um código exclusivo para cada objeto.
  • Uso equivalente de == e equals() . O operador == compara referências de objetos, enquanto o método equals() compara valores de objetos.

O que você precisa lembrar sobre equals () e hashCode ()


  • É recomendável que você sempre substitua os métodos equals() e hashCode() em seus POJOs ( russo , inglês )
  • Use um algoritmo eficiente para criar um código de hash exclusivo.
  • Ao substituir o método equals() , sempre substitua o hashCode() .
  • O método equals() deve comparar o estado completo dos objetos (valores dos campos).
  • O hashCode() pode ser um identificador POJO (ID).
  • Se o resultado da comparação do código de hash dos dois objetos for false , o método equals() também deverá ser false .
  • Se equals() e hashCode() não hashCode() redefinidos ao usar coleções de hash, a coleção terá elementos duplicados.

Saiba mais sobre Java



Tradicionalmente, aguardo seus comentários e convido você para uma aula aberta , que será realizada por nossa professora Sergei Petrelevich em 18 de março

Source: https://habr.com/ru/post/pt443710/


All Articles