CodeGym /Blogue Java /Random-PT /Métodos equals e hashCode: práticas recomendadas
John Squirrels
Nível 41
San Francisco

Métodos equals e hashCode: práticas recomendadas

Publicado no grupo Random-PT
Oi! Hoje falaremos sobre dois métodos importantes em Java: 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... Métodos equals e hashCode: práticas recomendadas - 1Na aula de hoje, falaremos sobre esses conceitos em detalhes. E acredite, temos o que conversar! Mas antes de passarmos para o novo, vamos atualizar o que já cobrimos :) Como você se lembra, geralmente é uma má ideia comparar dois objetos usando o ==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 Carobjetos 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 car1e car2apontam 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: o equals()método é definido na Objectclasse. 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 Objectclasse. 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? Métodos equals e hashCode: práticas recomendadas - 2Claro, apenas um teste de DNA pode fornecer uma garantia. Duas pessoas podem ter a mesma cor de olhos, corte de cabelo, nariz e até mesmo cicatrizes - há muitas pessoas no mundo e é impossível garantir que não haja doppelgängers por aí. Mas precisamos de um mecanismo confiável: somente o resultado de um teste de DNA nos permitirá tirar uma conclusão precisa. O que isso significa para o nosso equals()método? Precisamos substituí-lo noManclasse, tendo em conta os requisitos do nosso programa. O método deve comparar o int dnaCodecampo 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 Manclasse, temos uma Womanclasse 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 Manclasse 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
...então procedemos a uma comparação das características relevantes. Para nós, isso significa os dnaCodecampos dos dois objetos. Ao substituir o equals()método, certifique-se de observar estes requisitos:
  1. 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;
    

  2. Simetria.

    Se a.equals(b) == true, então b.equals(a)deve 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, então b.equals(c)também deve retornar verdadeiro.

  4. 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 de equals()deve ser sempre o mesmo.

  5. 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 o hashCode()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:
  1. 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 do equals()método.

  2. Se o hashCode()método 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 na direção oposta. Dois objetos diferentes podem ter o mesmo código hash.

A terceira regra é um pouco confusa. Como isso pode ser? A explicação é bem simples. O 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 Personobjeto (semelhante à Manclasse). 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 converterPersonobjetos 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 Personclasse? 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 Personobjetos 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. Métodos equals e hashCode: melhores práticas - 4Em nosso exemplo anterior, com a Personclasse, descartamos a maioria dos campos (ou seja, características humanas) como insignificantes e usamos apenas osdnaCodecampo nas comparações. Agora estamos trabalhando em um reino muito idiossincrático, no qual não há detalhes insignificantes! Aqui está nossa LuxuryAutoaula:

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 modelcampo em nossa classe é uma String. Isso é conveniente porque a Stringclasse já substitui o hashCode()método. Calculamos o modelcó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 LuxuryAutoclasse. O hashCode()método permaneceu inalterado, mas removemos o modelcampo 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!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION