Java equals() e hashCode()

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

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 Java do método equals(), qualquer implementação deve aderir aos 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 se e somente se y.equals(x) retornar true.
  • Para múltiplos objetos x, y e z, se x.equals(y) retorna true e y.equals(z) retorna 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 está sendo usada na implementação de equals().
  • A implementação do método equals() da classe Object retorna true apenas quando ambas as referências estão apontando para o mesmo objeto.

Java hashCode()

O método nativo Java Object hashCode() 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 múltiplas execuções da mesma aplicação.
  • Se dois objetos são iguais de acordo com o método equals(), então seus códigos hash devem ser iguais.
  • Se dois objetos são diferentes de acordo com o método equals(), seus códigos hash não são obrigados a 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() do 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 essas regras.

  • Se o1.equals(o2), então o1.hashCode() == o2.hashCode() deve sempre ser true.
  • Se o1.hashCode() == o2.hashCode é verdadeiro, isso 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. Note que o programa não lançará exceções se o contrato entre equals() e hashCode() for violado; se não tiver a intenção de usar a classe como chave em uma tabela de hash, isso não causará problemas. No entanto, se a intenção for usar a classe como chave em uma tabela de hash, é imprescindível 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 executarmos o programa acima, será impresso null. Isso ocorre porque o método hashCode() padrão da classe Object é utilizado para encontrar o bucket onde a chave deve ser procurada. Como não temos acesso às chaves do HashMap e estamos criando a chave novamente para recuperar os dados, perceberemos que os valores de hash code 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 dos métodos equals() e hashCode(), mas se não os implementarmos com cuidado, pode haver problemas estranhos em tempo de execução. Felizmente, a maioria das 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 automáticas dos métodos equals() e hashCode().

@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;
}

Note 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 Project 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 tabela de hash em Java usam a seguinte lógica para operações de get e put.

  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, então adicione o objeto para a operação de put e retorne null para a operação de get.
  3. Se houver outros objetos no balde com o mesmo código hash, o método “key” equals entra em jogo.
    • Se equals() retornar true e for uma operação de colocação, então o valor do objeto é substituído.
    • Se equals() retornar false e for uma operação de colocação, uma nova entrada é adicionada ao balde.
    • Se equals() retornar true e for uma operação de obtenção, então o valor do objeto é retornado.
    • Se equals() retornar false 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 métodos equals() e hashCode() estão relacionados. O fenômeno quando duas chaves têm o mesmo código 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. Essa é a razão para o uso de números primos na geração do código hash, para que as entradas do mapa sejam distribuídas adequadamente entre todos os baldes.

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

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

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

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

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

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