HashCode.java: uma alternativa
Em uma das minhas últimas aquisições obtive o livro Java Efetivo de Joshua Bloch, uma referência para quem conhece Java e quer melhorar seu entendimento sobre detalhes da linguagem. O item 9 do 3º capítulo é um tanto interessante, explicando regras de sobreposição do método hashCode. Não vou transcrever aqui o que fala o livro, mas quero deixar uma alternativa que acho interessante para geração de hashCodes quando ocorre a sobreposição do método levando em consideração o que fala no livro. Conceitos como flexibilidade e manutenibilidade e até o princípio da Responsabilidade Única foram levados em consideração, já no quesito desempenho será apresentado uma alternativa.
O livro fala sobre como se deve implementar a conversão de atributos de uma classe em hashCode, o cálculo e o tratamento, mas sempre aprendi que a lógica de uma parte quando pode viver isolada deve ser isolada, isso aumenta a flexibilidade do sistema e consequentemente a manutenibilidade, com isso, a alternativa que apresento é uma classe HashCode contendo toda a lógica para anexar os atributos e obter um hashCode. Segue a classe:
public final class HashCode { private HashCode() {} public static HashCode create() { return new HashCode(); } private int _result = 17; public HashCode append(int value) { _result = 31 * _result + value; return this; } public HashCode append(boolean value) { return append(value?1:0); } public HashCode append(byte value) { return append((int)value); } public HashCode append(char value) { return append((int)value); } public HashCode append(short value) { return append((int)value); } public HashCode append(long value) { return append((int)(value^(value>>>32))); } public HashCode append(float value) { return append(Float.floatToIntBits(value)); } public HashCode append(double value) { return append(Double.doubleToLongBits(value)); } public HashCode append(Object value) { return append(value==null?0:value.hashCode()); } @Override public int hashCode() { return _result; } }
Perceba que utilizei alguns outros conceitos como um construtor estático, sobrecarga e fluent interfaces. A classe é final, pois acredito que ela não deva ser estendida e o próprio método hashCode de Object é sobrescrito aqui para retornar o resultado final.
Usabilidade
No caso de atributos onde não exista uma coleção o código ficaria como mostrado no exemplo abaixo.
@Override public int hashCode() { return HashCode.create() .append(_name) // String .append(_url) // URL .append(_descriptiveStatus) // String .append(_timer) // long .append(_effect) // boolean .hashCode(); }
Já no caso de uma coleção, preferi não implementar nada, pois o que deve ser levado em conta numa coleção somente cada programador saberá, lembrando que no caso de arrays a classe Arrays possui vários métodos para geração de hashCodes, vale a pena conferir. Quando existir uma coleção, prefiro iterar sobre ela e obter os atributos que considero melhor. Abaixo um exemplo:
@Override public int hashCode() { HashCode result = HashCode.create(); result.append(_nameBedroom); // String Enumeration<String> keys = _bedroom.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); result.append(_bedroom.get(key).hashCode()); // int } return result.hashCode(); }
Desempenho
Para testar o desempenho do código com e sem a classe HashCode fiz dois testes com jUnit, ambos efetuam o mesmo calculo, cada um sobre um for de 100 milhões de loops que proporciona uma visão mais humana, um dos testes não utiliza a classe HashCode, o outro sim.
Percebe-se a diferença de tempo, quase o dobro do tempo para executar o mesmo cálculo quando se utiliza a classe HashCode. This is bad!!!
public class HashCodeTest { String nome = "Vinicius Knob"; int anoNascimento = 1989; long dataHoje = Calendar.getInstance().getTimeInMillis(); boolean java = true; @Test public void testSemClasse() { for (int i = 0; i <= 100000000; i++) { int result = 17; result = 31 * result + nome.hashCode(); result = 31 * result + anoNascimento; result = 31 * result + (int)(dataHoje^(dataHoje>>>32)); result = 31 * result + (java?1:0); } } @Test public void testComClasse() { for (int i = 0; i <= 100000000; i++) { HashCode.create().append(nome) .append(anoNascimento).append(dataHoje) .append(java).hashCode(); } } }
Em um sistema onde o método hashCode é muito utilizado e/ou possui um cálculo custoso é necessário fazer uma escolha: não utilizar a classe HashCode ou utilizar o conceito de cache e armazenar na própria classe consumidora de HashCode um atributo contendo o hashCode gerado na primeira execução. Isso ajudaria muito, pois somente na primeira vez o hashCode seria gerado, enquanto nas próximas apenas consumido. Cuidado, caches desse tipo não são uma boa alternativa quando se trata de uma classe mutável, já que o hashCode pode estar baseado em atributos que são alterados entre uma solicitação e outra do método hashCode. Desempenho sempre é uma questão delicada.
Considerável ganho de desempenho quando utilizado cache e uma diferença de 3ms quando utilizado a classe HashCode.
Considerável ganho de desempenho quando utilizado cache e uma diferença de 3ms quando utilizado a classe HashCode.
public class HashCodeTest { String nome = "Vinicius Knob"; int anoNascimento = 1989; long dataHoje = Calendar.getInstance().getTimeInMillis(); boolean java = true; @Test public void testSemClasse() { int cacheResult = 0; for (int i = 0; i <= 100000000; i++) { if (cacheResult == 0) { int result = 17; result = 31 * result + nome.hashCode(); result = 31 * result + anoNascimento; result = 31 * result + (int)(dataHoje^(dataHoje>>>32)); result = 31 * result + (java?1:0); cacheResult = result; } } } @Test public void testComClasse() { int cacheResult = 0; for (int i = 0; i <= 100000000; i++) { if (cacheResult == 0) { cacheResult = HashCode.create().append(nome) .append(anoNascimento).append(dataHoje) .append(java).hashCode(); } } } }
Conclusão
Essa alternativa atende alguns requisitos para proporcionar flexibilidade e manutenibilidade ao código, porém seria interessante fornecer um atributo para servir como cache em casos onde cálculos exigem muito processamento, mesmo sem utilizar a classe. Alguns críticos podem considerar ela totalmente desnecessária, eu vejo como uma classe encapsuladora de um padrão documentado e que tecnicamente poderia ser seguido dessa forma. Alternativas para essa classe poderia proporcionar melhoria a flexibilidade ou até ao desempenho, uma estrutura diferente com nomes diferentes também é aceitável, mas tudo depende sempre de conhecimento, o que busco incansavelmente.
0 comentários: