Use isEmpty em vez de equals

04:58 Vinicius Knob 1 Comments


 "A perfeição é feita de pequenos detalhes - não é apenas um detalhe." (Michelangelo)

Gostaria de começar esse post ressaltando a famosa frase de Michelangelo ditada acima que fala sobre "detalhes". Em minha busca por conhecimento dedico meu tempo justamente a isso, detalhes. Não basta para mim ter a solução em mãos, é preciso mais, é preciso saber como chegar até esta solução, cada detalhe da implementação de um algoritmo é o que o faz ter usabilidade, flexibilidade e clareza, porém nesse post quero tratar de algo mais interessante, um detalhe que envolve desempenho.

Assim como qualquer linguagem, Java tem seus detalhes de implementação que gosto de explorar. Nesse post quero mostrar o porque deve ser usado isEmpty() em vez de equals(""). Primeiramente vamos a documentação de ambos os métodos:

isEmpty - "Returns true if, and only if, length() is 0".
equals - "Compares this string to the specified object. The result is true if and only if the argument is not null and is a String object that represents the same sequence of characters as this object".

Pela documentação de cada método já temos uma simples noção de implementação onde que isEmpty utiliza um atributo privado para verificar se é igual a zero, apenas isso enquanto equals utiliza verificação de argumento nulo e sequencia de caracteres, o que com certeza é uma implementação mais penosa para um caso tão simples como a verificação de uma string vazia. Equals abrange verificação para casos mais complexos. Vamos ao código de cada método:

isEmpty
public boolean isEmpty() {
    return count == 0;
}
equals
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = count;
        if (n == anotherString.count) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = offset;
            int j = anotherString.offset;
            while (n-- != 0) {
                if (v1[i++] != v2[j++])
                    return false;
            }
            return true;
        }
    }
    return false;
}

Como esperado, o método equals é dedicado a comparação de strings diversas, enquanto o método isEmpty ataca um problema específico onde posso apenas querer saber se essa string esta vazia ou não.

Desempenho


Para o teste de desempenho fiz dois testes com jUnit, cada um atacando um dos métodos. Cada teste possui um loop de 100 milhões de iterações mostrando uma diferença de tempo considerável. Acredito que grandes empresas com sites que superam acessos de 1 milhão de pessoas rodando diversas classes contendo diversas vezes uma comparação utilizando equals("") resulte em algo semelhante, senão pior.

public class StringEmptyTest {
    
    @Test
    public void isEmpty() {
        String test = "";
        for (int i = 0; i < 100000000; i++) {
            test.isEmpty();
        }
    }
    
    @Test
    public void equals() {
        String test = "";
        for (int i = 0; i < 100000000; i++) {
            test.equals("");
        }
    }

}


No resultado, isEmpty() foi 5,6 vezes menor em seu tempo de execução.

Conclusão


Quando Michelangelo cunhou sua frase, assim como tantas outras, o contexto era outro obviamente, mas programar não deixa de ser uma arte e assim como no contexto de Michelangelo, nós no nosso contexto precisamos entender o quanto um detalhe é importante. Com certeza utilizar isEmpty ou equals em um programa com poucos acessos não fará diferença, o que pretendo dizer com esse post é justamente o que a frase de Michelangelo diz "A perfeição é feita de pequenos detalhes...", junte esse detalhe, mais aquele outro e mais um e terá uma melhoria de desempenho. 

1 comentários:

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: