Vimos o critério de igualdade entre objetos varia de acordo com as necessidades de projeto quando estamos falando de programação. Agora, vamos abordar o método hashCode() para verificar o que ele tem haver com a comparação de objetos, para posteriormente, podermos 'fechar' a relação entre hashCode() e equals() analizando o contrato estabelecido para os dois em java no próximo post. Por hora, tenham em conta que existe um contrato de funcionalidade que amarra esses dois métodos no que diz respeito aos seus retornos. Eles precisam ser consistentes entre si, mesmo que a regra que os defina possa ser qualquer uma.
O código de espalhamento (hashCode), em java, é um número que nos permitirá dividir os objetos de uma classe em 'grupos' segundo um critério que precisa ser claro e consistente. Se pegarmos todos os objetos de uma classe no momento da execução e olharmos para eles sem um critério que os separem entre si, estaremos apenas olhando para um grupo específico de objetos daquela classe existentes naquele momento: o todo. Para termos grupos menores, precisamos estabelecer um critério que coloque cada objeto num subgrupo. Inicialmente, podemos perguntar porque estamos preocupados com isso, mas a resposta vem rapidamente quando começamos a trabalhar na área: em vários momentos, o desempenho da aplicação vai depender fortemente disso. De forma resumida (pois iremos ver isso com mais calma depois), precisamos agrupar objetos em grupos menores que o todo quando estamos trabalhando com coleções grandes desses objetos.
Como exemplo, vamos pensar numa versão de hashCode() para a nossa classe Algo:
@Override public int hashCode() { // Se o nome, apesar de não nulo, for vazio, o 'grupo' será zero. if ("".equals(nome)) { return 0; } /* * ATENÇÂO: Todo caractere é um número inteiro. A tabela de caracteres * (charset) que estiver em uso no momento é que vai mapear cada caractere * com o seu valor. No entanto, durante a execução, essa tabela não vai * mudar. Então, posso confiar que para um determinado caractere, o * inteiro correspondente será sempre o mesmo. * * Retorno o inteiro correspondente ao meu primeiro caractere, qualquer * que seja ele. */ return Character.getNumericValue(nome.toCharArray()[0]); }
Obs.: Todo caractere internamente é representado por um inteiro. Teste o seguinte: System.out.println('a' + 'b');. No meu caso aqui, com o charset que estou usando, a saída no console foi 195. Observe que 'somei' dois caracteres e não duas strings de um caractere cada. Sabemos que estamos falando de caracteres pelas aspas simples. Se executasse a instrução com strings, a saída seria 'ab'. Isso acontece porque em java o operador +, somente no caso das strings, é sobrecarregado nativamente na linguagem, e assim, a 'soma de strings' se converte em concatenação.
O método hashCode() devolve um inteiro, cujo sentido tentaremos compreender. Também é definido em Object, e portanto, lá ele tem uma implementação válida tanto do ponto de vista sintático (devolve um inteiro), quanto do ponto de vista semântico (respeita o contrato de hashCode() e equals()). Novamente, se ele não for sobrescrito incorretamente por uma superclasse que não Object, estará validamente disponível em qualquer classe java.
O inteiro devolvido pelo hashCode() indica o 'grupo' daquele objeto. Assim, objetos diferentes podem ter o mesmo hashCode. No nosso exemplo, eu tenho uma propriedade 'nome' do tipo String para basear a forma como vou separar meus objetos em grupos. O hashCode() sempre fará uso de alguma propriedade do seu objeto. No nosso caso, usaremos a propriedade 'nome' do Algo. É importante dizer neste momento que o que você venha a fazer com essa String para obter o seu hashCode pode ser praticamente qualquer coisa, desde que você devolva um inteiro e respeite o contrato de hashCode() e equals(). No nosso caso agora, escolhi devolver o inteiro que corresponde ao primeiro caractere do nome do meu Algo. Isso vai separar os meus Algos 'alfabeticamente', informando que todos os Algos que têm nome começando com 'a' vão ser do mesmo 'grupo'. Não é exatamente alfabético porque o hashCode é numérico e a definição da minha classe permite um nome começando com outros caracteres como numeros ou caracteres especiais etc. Mas isso não quebra o contrato, uma vez que esses objetos serão agrupados entre si.
A funcionalidade provida pelo hashCode() tem sido referida neste post como 'agrupar' apenas na tentativa de facilitar a compreensão da funcionalidade esperada desse método. No entanto, o propósito dele é o de espalhar o grande grupo de todos os objetos de uma classe em grupos menores. Além disso, encontraremos no uso do hashCode(), a necessidade de produzir um espalhamento sempre o mais eficiente possível para podermos tirar vantagem das coleções que fazem o uso de hashing, isto é, espalhamento.
A implementação acima do hashCode() é ineficiente para uso em uma aplicação real. Estou esperando que ela seja eficiente no intuito de esclarecer o funcionamento esperado do método. No entanto, apesar de ineficiente, ela é válida. Quando chegarmos ao contrato, tentaremos diferenciar o que é válido do que é eficiente. Mas porque ela é ineficiente? Quando temos coleções gigantes de objetos, da ordem de centenas de milhares, torna-se muito caro, em termos de processamento, encontrar um objeto específico dentro de um conjunto como esse por meios normais, isto é, iterando a coleção inteira. Com um hashCode que separa centenas de milhares de objetos em 26 grupos, imaginando nomes que começam apenas com letras (nosso exemplo), a iteração no subgrupo continua cara, pois o subgrupo continua com muitos milhares de objetos. Quando pensamos em muitos usuários procurando objetos em coleções grandes dentro de um servidor o problema se torna bastante real.
Neste momento encontramos um limite na explicação do hashCode(). Completaremos a explicação em duas partes: No próximo post, quando veremos o contrato que une hashCode() e equals() e quando abordarmos as coleções e mapas do tipo hash. Antes das coleções, no entanto, passaremos pelas interfaces Comparable e Comparator, que junto com hashCode() e equals() formam o quarteto necessário para a correta utilização dos vários tipos de coleções e mapas da linguagem java.
Nenhum comentário:
Postar um comentário