15 de mar. de 2012

Lanço uma Exceção ou não? Depende.

Esses dias estava conversando na aula de Aspectos Avançados sobre o tratamento de erros em métodos.

Tipicamente e basicamente, existem duas formas de lidar com isso no Java: com exceções ou sem exceções. Simples assim.

É possível usar, tradicionalmente, um código de erro no retorno, ou retornar um valor nulo ou equivalente, que é a melhor opção (e única) para linguagens que não possuem estruturas de Exceção.

Ou, a cada parâmetro inválido ou problema na computação de um resultado, lançar uma Exceção apropriada a ser tratada (ou não) pelo chamador.

Tá, e daí? Quais são as diferenças práticas?

Bem, claramente o uso de Exceções enriquece o tratamento, já que podem ser personalizadas e enquanto objetos podem carregar diversas informações sobre o erro. Por exemplo, uma mesma exceção de acesso a um SGBD pode carregar, além da mensagem de erro, o código de erro SQL, a instrução submetida ao banco, a versão do banco, etc.

Por outro lado, sendo objetos, criar e lançar Exceções tem um custo de processador, para criar, inicializar e destruir o objeto, e memória, para armazená-lo. Ou seja, usar Exceções implica em um decréscimo de performance.

Não, não sou contra o uso de Exceções, não me entenda mal. Como pensador de projetos de sistemas quero colocar que devemos avaliar os prós e contras de diversas abordagens e, baseado nesses prós e contras, tomar decisões.

Neste caso, temos (mais) um problema de "cobertor curto": maior riqueza de informação (bom), maior uso de recursos (cpu, mem, disk, ou seja, ruim), menor riqueza de informação (ruim), menor uso de recursos (bom). Enfim, é um dilema.

Para ilustrar uma diferença de performance no uso ou não de Exceções criei um pequeno programa para calcular volume. Ele simplesmente multiplica três inteiros, desde que sejam positivos e aí está o tratamento de erro, zero ou números negativos não são permitidos. O código-fonte está aqui: ExPerfTest.java

Na primeira abordagem, um parâmetro zero ou negativo invalida o cálculo e sempre retorna 0 como volume. Ou seja, se eu chamar volume(10, -20, 30) ele retorna 0, não lança Exceções. No meu Notebook, a execução deste método 100 mil vezes com valor inválido é realizada em 8 milissegundos em média. A seguir está o método:
public static int volume(int a, int b, int c) {
    return a < 0 ? 0 : b < 0 ? 0 : c < 0 ? 0 : a * b * c;
}
Na segunda abordagem, um parâmetro zero ou negativo invalida o cálculo e lança uma Exceção. Ou seja, se eu chamar volume(10, -20, 30) ele cria (new) e lança (throw) um objeto da classe Exception. No meu Notebook, a execução deste método 100 mil vezes com valor inválido é realizada em 190 milissegundos em média. A seguir está o método:
public static int volume2(int a, int b, int c) throws Exception {
    if (a < 0 || b < 0 || c < 0) throw new Exception("número inválido");
    return a * b * c;
}
Claro, tenho que deixar claro que, neste projeto, há uma sobrecarga da própria instrumentação (em especial o Late Binding da Interface). Na prática esses tempos são menores, mas aproximadamente na mesma proporção, o que não muda o fato de Exceções reduzirem a performance. O projeto inteiro (no NetBeans) pode ser baixado neste link: exception-perf-test.zip

Finalizando, é importante deixar claro que a redução de performance é visível em várias chamadas ao método. Executando o mesmo método com e sem exceções apenas 3 vezes a diferença é praticamente inexistente ( < 1ms ). Estes dados indicam que métodos de acesso aleatório ou pontual podem lançar exceções sem problemas. Para métodos que são acessados frequentemente ou recorrentemente, e provavelmente podem receber dados inválidos, pode ser escolhido uma implementação sem Exceção.

Cada caso é um caso, mas só um lembrete: Exceções são para situações Excepcionais. Se a Exceção acontece com frequência, ela não é Excepcional, concorda?

Quero agradecer ao Everton e Cristiano pela conversa em aula e que me levou a criação deste Post, obrigado.
Abraços,
Márcio Torres

Nenhum comentário: