Java equals() e hashCode()

Os métodos equals() e hashCode() do Java estão presentes na classe Object. Portanto, toda classe Java obtém a implementação padrão de equals() e hashCode(). Neste post, vamos analisar em detalhes os métodos equals() e hashCode() do Java.

Java equals()

A classe Object define o método equals() da seguinte forma:

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

De acordo com a documentação do Java sobre o método equals(), qualquer implementação deve seguir os seguintes princípios.

  • Para qualquer objeto x, x.equals(x) deve retornar true.
  • Para dois objetos x e y, x.equals(y) deve retornar true apenas se y.equals(x) retornar true.
  • Para múltiplos objetos x, y e z, se x.equals(y) retornar true e y.equals(z) retornar true, então x.equals(z) deve retornar true.
  • Múltiplas invocações de x.equals(y) devem retornar o mesmo resultado, a menos que alguma das propriedades do objeto seja modificada e usada na implementação do método equals().
  • A implementação do método equals() da classe Object retorna true apenas quando as duas referências estão apontando para o mesmo objeto.

Java hashCode()

O método Java Object hashCode() é um método nativo e retorna o valor do código hash inteiro do objeto. O contrato geral do método hashCode() é:

  • Múltiplas invocações de hashCode() devem retornar o mesmo valor inteiro, a menos que a propriedade do objeto que está sendo usada no método equals() seja modificada.
  • O valor do código hash de um objeto pode mudar em várias execuções do mesmo aplicativo.
  • Se dois objetos forem iguais de acordo com o método equals(), então seus códigos hash devem ser iguais.
  • Se dois objetos forem diferentes de acordo com o método equals(), seus códigos hash não precisam ser diferentes. Seu valor de código hash pode ou não ser igual.

Importância do método equals() e hashCode()

Os métodos hashCode() e equals() de Java são usados em implementações baseadas em tabelas de hash em Java para armazenar e recuperar dados. Eu expliquei detalhadamente em Como o HashMap funciona em Java? A implementação de equals() e hashCode() deve seguir estas regras.

  • Se o1.equals(o2), então o1.hashCode() == o2.hashCode() deve sempre ser true.
  • Se o1.hashCode() == o2.hashCode é verdadeiro, não significa que o1.equals(o2) será true.

Quando substituir os métodos equals() e hashCode()?

Quando substituímos o método equals(), é quase necessário substituir também o método hashCode(), para que o contrato entre eles não seja violado pela nossa implementação. Observe que o programa não lançará exceções se o contrato entre equals() e hashCode() for violado. Se não planeja usar a classe como chave de uma tabela de hash, isso não causará problemas. No entanto, se planeja usar a classe como chave, é necessário substituir ambos os métodos equals() e hashCode(). Vamos ver o que acontece quando confiamos na implementação padrão dos métodos equals() e hashCode() e usamos uma classe personalizada como chave em um HashMap.

package com.journaldev.java;

public class DataKey {

	private String name;
	private int id;

        // métodos getter e setter

	@Override
	public String toString() {
		return "DataKey [name=" + name + ", id=" + id + "]";
	}

}
package com.journaldev.java;

import java.util.HashMap;
import java.util.Map;

public class HashingTest {

	public static void main(String[] args) {
		Map<DataKey, Integer> hm = getAllData();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		Integer value = hm.get(dk);

		System.out.println(value);

	}

	private static Map<DataKey, Integer> getAllData() {
		Map<DataKey, Integer> hm = new HashMap<>();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		hm.put(dk, 10);

		return hm;
	}

}

Ao executar o programa acima, ele imprimirá null. Isso ocorre porque o método hashCode() do objeto é usado para encontrar o bucket onde a chave está localizada. Como não temos acesso às chaves do HashMap e estamos criando a chave novamente para recuperar os dados, perceberá que os valores de código hash de ambos os objetos são diferentes, e, portanto, o valor não é encontrado.

Implementando os métodos equals() e hashCode()

Podemos definir nossa própria implementação para os métodos equals() e hashCode(), mas se não os implementarmos com cuidado, pode causar problemas estranhos em tempo de execução. Felizmente, a maioria dos IDEs hoje em dia oferece maneiras de implementá-los automaticamente e, se necessário, podemos alterá-los de acordo com nossos requisitos. Podemos usar o Eclipse para gerar automaticamente os métodos equals() e hashCode(). Aqui estão as implementações dos métodos equals() e hashCode() geradas automaticamente.

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + id;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	DataKey other = (DataKey) obj;
	if (id != other.id)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

Observe que ambos os métodos equals() e hashCode() estão usando os mesmos campos para os cálculos, para que seu contrato permaneça válido. Se você executar o programa de teste novamente, obteremos o objeto do mapa e o programa imprimirá 10. Também podemos usar o Projeto Lombok para gerar automaticamente as implementações dos métodos equals() e hashCode().

O que é Colisão de Hash

Em termos muito simples, as implementações de tabelas de hash em Java usam a seguinte lógica para operações de obtenção e inserção.

  1. Primeiro, identifique o “Bucket” a ser usado usando o código hash da “chave”.
  2. Se não houver objetos presentes no bucket com o mesmo código hash, adicione o objeto para a operação de inserção e retorne nulo para a operação de obtenção.
  3. Se houver outros objetos no balde com o mesmo código de hash, então o método “key” equals entra em jogo.
    • Se equals() retornar verdadeiro e for uma operação de colocação, então o valor do objeto é substituído.
    • Se equals() retornar falso e for uma operação de colocação, então uma nova entrada é adicionada ao balde.
    • Se equals() retornar verdadeiro e for uma operação de obtenção, então o valor do objeto é retornado.
    • Se equals() retornar falso e for uma operação de obtenção, então null é retornado.

A imagem abaixo mostra os itens de um balde de HashMap e como seus equals() e hashCode() estão relacionados. O fenômeno quando duas chaves têm o mesmo código de hash é chamado de colisão de hash. Se o método hashCode() não for implementado corretamente, haverá um maior número de colisões de hash e as entradas do mapa não serão distribuídas corretamente, causando lentidão nas operações de obtenção e colocação. Esta é a razão para o uso de números primos na geração de código hash, para que as entradas do mapa sejam distribuídas corretamente entre todos os baldes.

O que acontece se não implementarmos tanto hashCode() quanto equals()?

Já vimos anteriormente que, se hashCode() não for implementado, não conseguiremos recuperar o valor, pois o HashMap utiliza o código hash para encontrar o bucket e procurar a entrada. Se usarmos apenas hashCode() e não implementarmos equals(), o valor também não será recuperado, pois o método equals() retornará falso.

Melhores Práticas para implementar os métodos equals() e hashCode()

  • Use as mesmas propriedades em ambas as implementações dos métodos equals() e hashCode(), para que seu contrato não seja violado quando qualquer propriedade for atualizada.
  • É melhor usar objetos imutáveis como chave da tabela de hash, para que possamos armazenar em cache o código hash em vez de calculá-lo em cada chamada. Por isso, a String é uma boa candidata para chave de tabela de hash, pois é imutável e armazena em cache o valor do código hash.
  • Implemente o método hashCode() para que ocorra o menor número possível de colisões de hash e as entradas sejam distribuídas uniformemente entre todos os buckets.

Você pode baixar o código completo em nosso Repositório no GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode