Olá!

Hoje falarei um pouco sobre os métodos equals() e hashCode(), assunto que não é novo mas ainda é muito utilizado e também é tratado na prova de certificação OCJP 6. 

Por mais que pareça estranho, ambos os métodos caminham juntos quando tratamos de Collections, e portanto, devem ser perfeitamente compreendidos para saber quando e como usá-los de forma eficiente.

equals()

Esse método é utilizado para verificar se dois objetos são significativamente iguais. Diferente do sinal "==" que verifica os bits do objeto (em outras palavras, se duas referências de objeto apontam para o mesmo objeto), o método equals() verifica o valor do objeto.

Esse método vem da classe Object, e na classe Object sua função é a mesma do sinal "==". As classes String e os Wrappers por sua vez já implementam sua sobrescrita afim de validar o valor significativo carregado no objeto.

Nota 1: É importante implementar sua sobrescrita em suas próprias classes justamente para verificar se dois objetos da classe em questão são iguais, do contrário, ele terá a mesma função do sinal "=="e dois objetos só serão considerados iguais se duas referências apontarem para o mesmo objeto.

Exemplo:
public class Teste {
    public static void main (String [] args) {
        Vaca um = new Vaca(8);
        Vaca dois = new Vaca(8);
        if (um.equals(dois)) {
            System.out.println("um e dois são iguais");
        }
    }
}

class Vaca {
    private int peso;

    Vaca(int val) {
        peso = val;
    }

    public int getPeso() {
        return peso;
    }

    public boolean equals(Object o) {
        if ((o instanceof Vaca) && (((Vaca)o).getPeso() == this.peso)) {
            return true;
        } else {
            return false;
        }
    }
}
Nota 2: equals() é público, e como tal, sua sobrescrita deve obedecer seu nível de acesso.

Pontos importantes do contrato de sobrescrita de equals():

  • Ele é reflexivo. Para qualquer referência de x, se x.equals(x), então tem que retornar true.
  • Ele é simétrico. Se x.equals(y) é true, então y.equals(x) também é true.
  • Ele é transitivo. Se x.equals(y) é true, e y.equals(z) também é true, então z.equals(x) também deve ser true.
  • Ele é consistente. Chamar x.equals(y) inúmeras vezes não irá alterar o valor do resultado, pois o método equals() não realiza nenhum tipo de modificação no objeto.
  • Se uma referência x possui valor não nulo, então x.equals(null) irá retornar false.

hashCode()

Hashcodes são geralmente utilizados para melhorar a performance em grandes coleções de dados. Em algumas coleções como HashMap e HashSet, o valor hashcode de um objeto é utilizado para determinar onde o objeto em questão será armazenado na coleção.


Para entendermos melhor a frase "[...] onde o objeto em questão será armazenado na coleção" temos que imaginar que as coleções previamente citadas armazenam um objeto em posições como slots, e quando querem recuperar esse objeto vão até esse slot e procuram pelo objeto.


Como podemos ver na imagem acima, é possível também que mais de um objeto seja guardado em uma mesma posição de slot (o slot é definido com base no código hashcode do objeto). Portanto, podemos concluir também que um código hashcode mal escrito (como um que retorna sempre o mesmo valor para o objeto), irá colocar todos os objetos no mesmo slot da coleção, o que acarretará em perda de performance (apesar de programaticamente ser um código válido).

Nota: Se uma classe não implementar a sobrescrita de hashCode(), então será gerado um hashcode único para cada objeto.

Certo, mas aonde entra a parte de equals() caminhar junto com hashCode()?

Simples. Quando tratamos de coleções, vimos que algumas coleções armazenam os objetos mediante o hashcode do mesmo. Vamos voltar ao exemplo do código acima da Vaca. Agora imagine que queiramos verificar se dois objetos Vaca são iguais mediante verificação do peso, ou seja, se o peso de dois objetos forem iguais, então determinamos igualdade para os objetos. Se adicionarmos os objetos em uma coleção do tipo HashSet por exemplo (que irá guardar o objeto através do hashcode) e a classe Vaca não sobrescreveu o método hashCode() (o que irá retornar um hashcode único para cada objeto), então cada objeto será armazenado em um slot único da coleção. O problema é que quando queremos verificar se um objeto é igual ao outro estando em uma coleção, primeiramente o objeto é procurado no slot que foi armazenado mediante o hashcode e depois comparado os valores armazenados dentro do objeto mediante equals(). Mas como vamos encontrar um objeto no slot se a cada objeto novo é gerado um hashcode único? Exatamente, NUNCA iremos encontrar, logo, equals() NUNCA retornará true.

Um bom código de hashCode() não deve ser único para cada objeto criado, e também não deve retornar sempre o mesmo valor (pois assim armazenaria todos os objetos no mesmo slot perdendo performance).

Portanto, melhorando o código da classe Vaca, poderíamos ter:
class Vaca {
    private int peso;

    Vaca(int val) {
        peso = val;
    }

    public int getPeso() {
        return peso;
    }

    public boolean equals(Object o) {
        if ((o instanceof Vaca) && (((Vaca)o).getPeso() == this.peso)) {
            return true;
        } else {
            return false;
        }
    }    
    
    public int hashCode() { 
        return (peso * 17); 
    }
}
Pontos importantes do contrato de sobrescrita de hashCode():

  • Independente de quantas vezes o método hashCode() for chamado durante uma execução de uma aplicação Java, o valor inteiro retornado deve ser sempre o mesmo. Esse valor não precisa ser o mesmo durante outras execuções da aplicação.
  • Se dois objetos são significativamente iguais mediante execução do método equals(), então o hashCode() de ambos devem retornar o mesmo valor.
  • Se dois objetos são significativamente diferentes mediante execução do método equals(), não significa que o método hashCode() também deva retornar valores inteiros diferente. No entanto, se voltar, pode melhorar a performance de busca do objeto.

Por hoje é só pessoal, qualquer dúvida ou sugestão deixem nos comentários.
Até a próxima!


Sobre o Autor:
Guilherme Oliveira
Guilherme Oliveira trabalha com desenvolvimento de software a mais de 10 anos. É técnico em informática, bacharel em ciência da computação, especialista em engenharia de software, além de ser Oracle Certified Java Programmer 6 (OCJP 6). Suas áreas preferidas são desenvolvimento web e games.