O códigos de hashing são geralmente utilizados para determinar em qual local, no conjunto, ele deve ser armazenado. Sendo, posteriormente, utilizado para fazer uma pesquisa dentro do próprio conjunto. Uma forma bastante comum de se utilizar o hashCode é definindo um número para cada letra do Alfabeto (A=1, B=2, C=3 e etc..). Ao ser fornecido um nome, o algorítmo irá calcular a multiplicação dos números referentes a letras e irá retornar um hashCode daquele nome, em específico.

Digamos que um cliente não muito ortodoxo solicite que, em seu congresso, além do cadastro comum, seja criado em paralelo um cadastro de pessoas com nomes cuja quantidade de letras, multiplicada por 8, seja a chave para seu posicionamento em uma tabela hashing. Será necessário que sobrescreva o método hashCode() da classe Congressista para que seja feita esta inserção e posterior comparação. Para facilitar, também deverá ser sobrescrito o método equals(), caso o cliente decida pesquisar por nomes específicos com um determinado valor.

Existem pessoas que fazem PhD para desenvolver algorítimos de hashing beirando a perfeição. Como não é esta a intenção do nosso artigo, trabalharemos com algo simples, como exemplificado acima. Além do nome do congressista que será utilizado para fazer o cálculo do hashing, será preciso também alguma variável que, comparada com outro objeto, sirva para definir que o objeto procurado foi encontrado. No nosso caso, coloquei o CPF do congressista.

Por que precisamos desta variável? Existe um problema com o hashing, se ainda não foi percebido, caso seja utilizada esta lógica que foi sugerida. Como iremos multiplicar a quantidade de letras por 8, terão nomes que resultarão no mesmo valor. Por exemplo, Tiago=5, Livia=5 (5x8=40) -> Tiago = 40, Livia = 40. Então, os dois estarão no mesmo slot mesmo sendo congressistas diferentes. Iremos entender mais este problema no desenrolar do código.

A primeira coisa a se fazer é criar a classe Congressista e implementar tanto o algoritmo que calculará o hashing - sobrescrevendo hashCode() - quanto sobrescrever equals() para encontrar dentro do slot o congressista que procuramos.

Passo 1 - Implementando hashCode():

Para implementar o hashCode() é necessário prestar atenção nas regras de sobrescrição para que se escreva uma sintaxe válida. De acordo com a documentação do Java, esta é a sobrescrição correta do método:

Listagem 1: Sintaxe de sobrescrição do método hashCode

public int hashCode()
{
// Agoritimo de criação de hashing aqui.
}

Sabendo disto, essa seria a nossa implementação do método hashCode() na classe Congressista.java

Listagem 2: Implementação do método hashCode()

public int hashCode()
{
	return getNome().length() * 8;
}

Como o esperado, nós contabilizamos a quantidade de letras que o nome possui e multiplicamos o resultado por 8. Assim, teremos o nosso hashing code.

Com isto, na hora de inserir e buscar por objetos dentro de uma tabela hashing, sempre será utilizado este mesmo mecanismo para que seja obtido sempre o mesmo resultado.

Passo 2: Implementando o método equals()

Assim como o hashCode(), também deverá seguir a regra de sobrescrição com o método equals(). Segundo a API do Java, esta é sua assinatura correta:

Listagem 3: Assinatura do método equals

public boolean equals(Object o) {}

Sendo assim, vejamos como ficaria a implementação do método equals() na nossa classe Congressista.java

Listagem 4: Implementação do método equals()

public boolean equals(Object o)
{
	if ((o instanceof Congressista) &&
			((Congressista) o).getNome().equals(this.getNome()))
	{
		return true;
	}else
		return false;
}

Neste método é verificado se o objeto é, de fato, um Congressista e se possui o mesmo nome que o objeto que foi inserido como exemplar de busca. Ao efetuar uma busca na tabela hashing utilizando um objeto Congressista com o nome Carlos, por exemplo, será, primeiramente, efetuada uma busca pelo slot que contenham nomes com o resultado 48 (Carlos = 6. 6*8 = 48). Encontrado o slot, será utilizado o método equals() de cada objeto dentro do slot para verificar se o objeto lá dentro é igual ao objeto que estamos procurando. Caso encontre, será retornado true a o objeto em questão, assim como o valor atribuido a ele, foi encontrado!

Segue o código da classe Congresissta.java completo:

Listagem 5: Código da classe Congressista.java

public class Congressista {

	private String nome;
	private long cpf;

	public Congressista(String nome, long cpf)
	{
		this.cpf = cpf;
		this.nome = nome;
	}

	public String getNome()
	{
		return this.nome;
	}

	public long getCpf()
	{
		return this.cpf;
	}


	public boolean equals(Object o)
	{
		if ((o instanceof Congressista) &&
				((Congressista) o).getNome().equals(this.getNome()))
		{
			return true;
		}else
			return false;
	}

	public int hashCode()
	{
		return getNome().length() * 8;
	}
}

Passo 3: Criação e implementação de uma tabela de hashing

Nesta etapa, será utilizado HashMap<Congressista,String> para complementar o nosso exemplo. Aos não familiarizados com o conjunto HashMap, ele utilizará o primeiro objeto como chave e o segundo como valor agregado a esta chave. Ou seja, no nosso caso HashMap<Congressista,String>, Congressita será a chave da nossa busca e a String será o valor atrelado a este objeto encontrado.

Eis a criação do nosso HashMap:

Listagem 6: Código da classe Teste

import java.util.HashMap;

public abstract class Teste{

	public static void main(String[] args) {

		Congressista con1 = new Congressista("Tiago", 1234567);
		Congressista con2 = new Congressista("Caio", 76543221);

		HashMap<Congressista,String> hash = new HashMap<Congressista,String>();

		// Inserção dos congressistas no HashMap
		hash.put(con1, "Info importante sobre Tiago");
		hash.put(con2, "Info importante sobr Caio");

		/*
		 * Criaremos um terceiro congressista e iremos efetuar uma busca
		 * para ver se o algoritimo está, de fato, funcionando.
		 * Não será encontrado o objeto porque Primeiro: não o inserimos no HashMap
		 * Segundo: o tamanho do nome não bate com nenhum tamanho de nome inserido.
		 */

		Congressista con3 = new Congressista("Felipe", 123123123);

		System.out.println(hash.containsKey(con3)); // False

		/*
		 *  Aqui iremos procurar por um nome que coincida com o valor de algum
		 *  que ja esteja cadastrado, Mas - muito importante - não bate com o nome
		 *  do congresissta que está lá. Por isto é importante a implementação do
		 *  método equals()
		 */

		Congressista con4 = new Congressista("Livia", 54545432);

		System.out.println(hash.containsKey(con4)); // Falso!

		/*
		 *  Por fim, iremos pesquisar por um objeto que já existe e exibir suas info.
		 */

		String info = hash.get(con1); // retornará o valor atribuído ao objeto no Mapa
		System.out.println(info);

		info = hash.get(con2);
		System.out.println(info);

	}

}

Como explicam os comentários no código, é de extrema importância que o método equals seja sobrescrito para que a busca seja mais eficiente. Pois poderemos até encontrar um valor igual para nomes diferentes, mas, jamais, irá retornar true se o nome não for o mesmo.

Resultado do código acima:

Resultado da execução da classe Teste.java

Figura 1: Resultado da execução da classe Teste.java

Contrato de hashCode()

De acordo com a documentação do Java, existe um contrato a ser seguido caso seja sobrescrito o hashCode():

  • Sempre que for chamado no mesmo objeto mais de uma vez durante a execução de um aplicativo Java, o método hashCode() terá que retornar consistentemente o mesmo inteiro, contanto que nenhuma informação usada nas comparações de equals() envolvendo o objeto tenha sido alterada. Este inteiro terá que permanecer constante de uma execução a outra do mesmo aplicativo.
  • Se dois objetos forem iguais de acordo com o método equals(Object), então, a chamada do método hashCode() nos dois objetos deve produzir como resultado o mesmo inteiro.
  • Não é obrigatório que quando dois objetos forem diferentes de acordo com o método equals(Object), a chamada ao método hashCode() nesses objetos produza resultados inteiros distintos. No entanto, o programador deve ficar alerta para o fato de que produzir resultados inteiros distintos para objetos diferentes pode melhorar o desempenho das tabelas de hashing.

Observação: Cuidado ao utilizar variáveis transient como hashCode(). Pois, como é sabido, ao serem reativadas elas retornam com seus valores resetados.

Com isto, finalizo o meu artigo com a esperança de que tenha conseguido passar o meu conhecimento da melhor maneira possível. Bons estudos a todos!

Abraço!

Referências: