HashCode.java: uma alternativa

12:05 Vinicius Knob 0 Comments

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.

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: