JavaRush /Blogue Java /Random-PT /Métodos equals e hashCode: prática de uso

Métodos equals e hashCode: prática de uso

Publicado no grupo Random-PT
Olá! Hoje falaremos sobre dois métodos importantes em Java - equals()e hashCode(). Esta não é a primeira vez que os conhecemos: no início do curso JavaRush houve uma pequena palestra sobre equals()- leia se você esqueceu ou não viu antes. Métodos é igual a &  hashCode: prática de uso - 1Na lição de hoje falaremos detalhadamente sobre esses conceitos – acredite, há muito o que falar! E antes de passarmos para algo novo, vamos refrescar nossa memória com o que já abordamos :) Como você se lembra, a comparação usual de dois objetos usando o ==operador “ ” é uma má ideia, porque “ ==” compara referências. Aqui está nosso exemplo com carros de uma palestra 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 objetos idênticos da classe Car: todos os campos nas duas máquinas são iguais, mas o resultado da comparação ainda é falso. Já sabemos o motivo: os links car1apontam car2para endereços diferentes na memória, portanto não são iguais. Ainda queremos comparar dois objetos, não duas referências. A melhor solução para comparar objetos é o arquivo equals().

método igual()

Você deve se lembrar que não criamos esse método do zero, mas o substituímos - afinal, o método equals()é definido na classe Object. No entanto, na sua forma habitual é de pouca utilidade:
public boolean equals(Object obj) {
   return (this == obj);
}
É assim que o método equals()é definido na classe Object. A mesma comparação de links. Por que ele foi feito assim? Bem, como os criadores da linguagem sabem quais objetos em seu programa são considerados iguais e quais não são? :) Esta é a ideia principal do método equals()- o próprio criador da classe determina as características pelas quais a igualdade dos objetos desta classe é verificada. Ao fazer isso, você substitui o método equals()em sua classe. Se você não entende muito bem o significado de “você mesmo define as características”, vejamos um exemplo. Aqui está uma classe simples de pessoa - 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.
}
Digamos que estamos escrevendo um programa que precisa determinar se duas pessoas são aparentadas por gêmeos ou apenas doppelgängers. Temos cinco características: tamanho do nariz, cor dos olhos, penteado, presença de cicatrizes e resultado de teste biológico de DNA (para simplificar - em forma de número de código). Quais destas características você acha que permitirão ao nosso programa identificar parentes gêmeos? Métodos é igual a &  hashCode: prática de uso - 2É claro que apenas um teste biológico pode fornecer uma garantia. Duas pessoas podem ter a mesma cor de olhos, penteado, nariz e até cicatrizes - existem muitas pessoas no mundo e é impossível evitar coincidências. Precisamos de um mecanismo fiável: só o resultado de um teste de ADN nos permite tirar uma conclusão precisa. O que isso significa para o nosso método equals()? Precisamos redefini-lo em uma aula Manlevando em consideração os requisitos do nosso programa. O método deve comparar o campo de int dnaCodedois objetos e, se forem 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. Perdemos alguma coisa. Neste caso, para os nossos objetos definimos apenas um campo “significativo” pelo qual a sua igualdade é estabelecida - dnaCode. Agora imagine que não teríamos 1, mas 50 desses campos “significativos”.E se todos os 50 campos de dois objetos forem iguais, então os objetos são iguais. Isso também pode acontecer. O principal problema é que calcular a igualdade de 50 campos é um processo demorado e que consome recursos. Agora imagine que além da classe, Mantemos uma classe Womancom exatamente os mesmos campos do Man. E se outro programador usar suas classes, ele poderá facilmente escrever em seu programa algo como:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

   Woman woman = new Woman(.........);//same bunch of parameters.

   System.out.println(man.equals(woman));
}
Nesse caso, não adianta verificar os valores dos campos: vemos que estamos olhando para objetos de duas classes diferentes, e eles não podem ser iguais em princípio! Isso significa que precisamos fazer uma verificação no método equals()– uma comparação de objetos de duas classes idênticas. 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 de outra coisa? Hmm... No mínimo, devemos verificar se não estamos comparando o objeto consigo mesmo! Se as referências A e B apontam para o mesmo endereço na memória, então são o mesmo objeto e também 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;
}
Além disso, não faria mal nenhum adicionar uma verificação para null: nenhum objeto pode ser igual a null, caso em que não há sentido em verificações adicionais. Levando tudo isso em consideração, nosso método equals()de classe Manficará 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. Se acontecer que:
  • comparamos dois objetos da mesma classe
  • este não é o mesmo objeto
  • não estamos comparando nosso objeto comnull
...então passamos a comparar características significativas. No nosso caso, os campos dnaCodede dois objetos. Ao substituir um método equals(), certifique-se de cumprir estes requisitos:
  1. Reflexividade.

    Qualquer objeto deve ser equals()para si mesmo.
    Já levamos esse requisito em consideração. Nosso método afirma:

    if (this == o) return true;

  2. Simetria.

    Se a.equals(b) == true, então b.equals(a)ele deverá retornar true.
    Nosso método também atende a esse requisito.

  3. Transitividade.

    Se dois objetos são iguais a algum terceiro objeto, então eles devem ser iguais entre si.
    Se a.equals(b) == truee a.equals(c) == true, a verificação b.equals(c)também deverá retornar verdadeiro.

  4. Permanência.

    Os resultados do trabalho equals()devem mudar somente quando os campos nele incluídos mudarem. Se os dados de dois objetos não foram alterados, os resultados da verificação equals()deverão ser sempre os mesmos.

  5. Desigualdade com null.

    Para qualquer objeto, a verificação a.equals(null)deve retornar falso.
    Este não é apenas um conjunto de algumas "recomendações úteis", mas um contrato estrito de métodos , prescrito na documentação do Oracle

Método hashCode()

Agora vamos falar sobre o método hashCode(). 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 a produtividade. Uma função hash, que é representada pelo método , em Java hashCode(), retorna um valor numérico de comprimento fixo para qualquer objeto. No caso de Java, o método hashCode()retorna um número de tipo 32 bits int. Comparar dois números entre si é muito mais rápido do que comparar dois objetos usando o método equals(), especialmente se ele usar muitos campos. Se nosso programa comparar objetos, é muito mais fácil fazer isso por código hash, e somente se eles forem iguais por hashCode()- prossiga para a comparação por equals(). A propósito, é assim que funcionam as estruturas de dados baseadas em hash – por exemplo, aquela que você conhece HashMap! O método hashCode(), assim como o equals(), é substituído pelo próprio desenvolvedor. E assim como for equals(), o método hashCode()possui requisitos oficiais especificados na documentação da Oracle:
  1. Se dois objetos forem iguais (ou seja, o método equals()retornar verdadeiro), eles deverão ter o mesmo código hash.

    Caso contrário, nossos métodos não terão sentido. A verificação por hashCode(), como dissemos, deve vir primeiro para melhorar o desempenho. Se os códigos hash forem diferentes, a verificação retornará falso, mesmo que os objetos sejam realmente iguais (conforme definimos no método equals()).

  2. Se um método hashCode()for chamado várias vezes no mesmo objeto, ele deverá retornar o mesmo número todas as vezes.

  3. A regra 1 não funciona ao contrário. Dois objetos diferentes podem ter o mesmo código hash.

A terceira regra é um pouco confusa. Como isso pode ser? A explicação é bastante simples. O método hashCode()retorna int. inté um número de 32 bits. Possui um número limitado de valores – de -2.147.483.648 a +2.147.483.647. Em outras palavras, existem pouco mais de 4 bilhões de variações do número int. Agora imagine que você está criando um programa para armazenar dados sobre todas as pessoas vivas na Terra. Cada pessoa terá seu próprio objeto de classe Man. Cerca de 7,5 bilhões de pessoas vivem na Terra. Em outras palavras, não importa quão bom seja o algoritmo Manque escrevemos para converter objetos em números, simplesmente não teremos números suficientes. Temos apenas 4,5 bilhões de opções e muito mais pessoas. Isso significa que não importa o quanto tentemos, os códigos hash serão os mesmos para pessoas diferentes. Esta situação (os códigos hash de dois objetos diferentes correspondentes) é chamada de colisão. Um dos objetivos do programador ao substituir um método hashCode()é reduzir ao máximo o número potencial de colisões. Como será o nosso método hashCode()para a classe Man, levando em conta todas essas regras? Assim:
@Override
public int hashCode() {
   return dnaCode;
}
Surpreso? :) Inesperadamente, mas se você olhar os requisitos verá que cumprimos tudo. Objetos para os quais o nosso equals()retorna verdadeiro serão iguais em hashCode(). Se nossos dois objetos Mantiverem valores iguais equals(ou seja, tiverem o mesmo valor dnaCode), nosso método retornará o mesmo número. Vejamos um exemplo mais complicado. Digamos que nosso programa deva selecionar carros de luxo para clientes colecionadores. Colecionar é algo complexo e possui muitos recursos. Um carro de 1963 pode custar 100 vezes mais que o mesmo carro de 1964. Um carro vermelho de 1970 pode custar 100 vezes mais que um carro azul da mesma marca e do mesmo ano. Métodos é igual a &  hashCode: prática de uso - 4No primeiro caso, com a classe Man, descartamos a maioria dos campos (ou seja, características da pessoa) como insignificantes e usamos apenas o campo para comparação dnaCode. Aqui estamos trabalhando com uma área muito singular, e não pode haver pequenos detalhes! Aqui está nossa aula LuxuryAuto:
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.
}
Aqui, na hora de comparar, devemos levar em consideração todos os campos. Qualquer erro pode custar centenas de milhares de dólares ao cliente, por isso é melhor estar 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 método, equals()não esquecemos todas as verificações de que falamos anteriormente. Mas agora comparamos cada um dos três campos dos nossos objetos. Neste programa, a igualdade deve ser absoluta, em todos os domínios. A respeito hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
O campo modelem nossa classe é uma string. Isso é conveniente: Stringo método hashCode()já foi substituído na classe. Calculamos o código hash do campo model, e a ele somamos a soma dos outros dois campos numéricos. Existe um pequeno truque em Java que é usado para reduzir o número de colisões: ao calcular o código hash, multiplique o resultado intermediário por um número primo ímpar. O número mais comumente usado é 29 ou 31. Não entraremos em detalhes da matemática agora, mas para referência futura, lembre-se de que multiplicar resultados intermediários por um número ímpar grande o suficiente ajuda a “espalhar” os resultados do hash funcionam e acabam com menos objetos com o mesmo código hash. Para o nosso método hashCode()no LuxuryAuto será 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 todos os meandros desse mecanismo neste post no StackOverflow , bem como no livro “ Effective Java ” de Joshua Bloch. Por fim, há mais um ponto importante que vale a pena mencionar. Cada vez que substituímos equals(), hashCode()selecionamos determinados campos do objeto, que foram levados em consideração nesses métodos. Mas podemos levar em conta diferentes campos 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()para hashCode()a classe LuxuryAuto. O método hashCode()permaneceu inalterado e equals()removemos o campo do método model. Agora, o modelo não é uma característica para comparar dois objetos por equals(). Mas ainda é levado em consideração no cálculo do código hash. O que obteremos como resultado? Vamos criar dois carros e conferir!
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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
Erro! Ao utilizar campos diferentes para eles equals(), hashCode()violamos o contrato estabelecido para eles! Dois objetos iguais equals()devem ter o mesmo código hash. Temos significados diferentes para eles. Tais erros podem levar às consequências mais incríveis, especialmente quando se trabalha com coleções que usam hashes. Portanto, ao redefinir equals()e hashCode()será correto utilizar os mesmos campos. A palestra acabou sendo bem longa, mas hoje você aprendeu muitas coisas novas! :) É hora de voltar a resolver problemas!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION