Exceções
Quando bem usadas, as exceções podem
melhorar a legibilidade, confiabilidade e sustentabilidade do
código. Mas quando usadas incorretamente, podem causar o efeito
oposto. Este artigo fornece orientações sobre seu uso.
Item 39: Use exceções
apenas para condições excepcionais
As exceções, como o próprio nome indica, devem ser usadas
apenas em condições excepcionais; nunca para controle de
fluxo comum.
Seu uso costuma ter um impacto negativo na performance, uma vez que
é custoso criar, lançar e pegar uma
exceção,
além de atrapalhar as otimizações feitas
automaticamente pela JVM.
O
uso de
exceções para controle de fluxo também mascara o
objetivo do código e aumenta a probabilidade de um bug passar
desapercebido, ficando mais difícil encontrar a causa do erro
posteriormente.
Lembre-se disso ao criar sua API, para não forçar o
cliente da classe a usar exceções desnecessariamente. Uma
classe que possui
um método que depende de um certo estado normalmente deve
incluir também um método separado para testar esse
estado, indicando se é apropriado ou não chamar o
primeiro método. Um exemplo desta técnica é a
classe Iterator, que possui
os métodos next e hasNext.Outra
alternativa é que o método que depende do estado retorne
um valor especial, como null, quando
chamado em um objeto em estado inapropriado. Esta segunda abordagem
é a mais adequada para casos em que o estado do objeto pode
mudar no intervalo entre a chamada do método teste e o
método que depende do estado, ou que a performance seja
crítica. Em outras situações, é melhor o
método teste, pois é mais legível e é mais
fácil detectar e corrigir um uso inapropriado.
Item 40: Use checked exceptions para
condições recuperáveis e runtime exceptions
para erros de programação
Há três tipos de throwables
na linguagem Java: checked
exceptions, runtime exceptions
e errors.
Utilize checked exceptions para condições em que
se espera que a classe que chamou o método possa recuperar-se.
O lançamento
da exceção é um indício
ao usuário da API que aquela determinada condição
é um dos retornos possíveis da chamada do método e
assim você força
que ele insira código para
tratar a exceção ou propagá-la.
Existem dois tipos de unchecked throwables: runtime exceptions e
errors. Eles são idênticos em seus comportamentos: ambos
são throwables que não precisam ser tratados. Quando um
programa lança algum deles, normalmente é um caso
impossível de recuperar; continuar executando apenas pioraria a
situação. Portanto, se o programa não incluir um
bloco catch para tratar esta exceção, a thread em que ela
acontece é interrompida mostrando uma mensagem de erro.
Utilize runtime exceptions para indicar erros de
programação. Normalmente são usadas em
violações de pré-condições, ou seja,
quando o cliente não adere ao contrato estabelecido na API.
Já os erros são utilizados pela JVM para indicar
uma deficiência de recursos, falhas ou outras
condições
que tornam impossível que a execução continue.
Ainda que isso não seja determinado na
especificação, é uma convenção bem
aceita, portanto você não deve criar novas classes de
erros; utilize subclasses de RuntimeException para todos os
unchecked throwables.
É possível definir throwables que não são
subclasses de Exception, RuntimeException ou Error. Seu
comportamento fica igual ao de checked exceptions, portanto é
melhor herdar da classe Exception, que é
o mais comum.
Resumindo, se você acha que a situação
provavelmente é recuperável, utilize checked exception,
caso contrário, utilize runtime exception.
Além disso, ao criar novas classes de exceções,
lembre-se de que
é possível incluir métodos para retornar
informações para o usuário, o que é uma
prática muito melhor do que simplesmente obrigá-lo a
obter os dados dentro da String que a representa. Isso é
especialmente importante nas checked exceptions, para que a causa do
problema seja identificada mais facilmente e contornada.
Item 41: Evite o uso desnecessário
de checked exceptions
As checked exceptions tornam o código mais confiável ao
forçar os programadores a lidar com condições
excepcionais. Porém, seu uso excessivo faz com que fique mais
desagradável trabalhar com a API, já que é
necessário incluir código para tratar ou propagar cada
exceção lançada.
Esse trabalho é justificável apenas se a
condição excepcional não pode ser prevenida com um
uso apropriado da API e o
programador pode tomar alguma ação útil quando
confrontado com esta exceção. Se o melhor que ele pode
fazer é um e.printStackTrace(), melhor
utilizar uma runtime exception.
O fardo é ainda maior quando é o caso de uma única
checked exception lançada por um método. Quando já
existem outras, a chamada ao método já estará
dentro de um bloco try. Então
bastaria colocar um novo bloco catch. Mas quando
uma exceção é sozinha responsável por
obrigar a existência bloco try, deve ser
considerado cuidadosamente se vale mesmo a pena incluí-la na
assinatura do método.
Uma técnica para transformar uma checked exception em runtime
exception é quebrar o método em duas partes, a primeira
retornando um boleano que indica se a exceção seria
lançada. Exemplo:
// Com
checked exception
try {
obj.actions(args);
} catch(TheCheckedException e) {
// trata a
condição
}
//Com teste de estado e runtime
exception
if (obj.actionPermitted(args)) {
obj.action(args);
} else {
//trata a
condição
}
Nem sempre esta transformação é apropriada,
conforme discutido no item 39. Mas quando é, torna a API mais
agradável de ser utilizada. Ainda que a segunda
seqüência para chamada do método não seja
muito melhor que a primeira, a API resultante é mais
flexível. Em casos que o programador tem certeza de que a
chamada será bem sucedida ou não se importa de deixar a
thread terminar caso a chamada falhe, bastaria chamar obj.action(args).
Item 42: Prefira o uso de
exceções padrão
O reuso de código é uma boa prática também
no caso de exceções. Torna a API mais fácil de
aprender e usar porque segue convenções que os
programadores já estão familiarizados. Um menor
número de classes de exceção também incorre
em menos
memória necessária e menos tempo gasto carregando classes.
A exceção mais comumente reutilizada é a IllegalArgumentException. Ela deve ser
lançada quando o método é chamado com um
argumento cujo valor seja inapropriado. Outra exceção
é a IllegalStateException, usada quando
a chamada ao método é ilegal naquela
situação, devido ao estado do objeto em que é
chamado. De forma geral, qualquer chamada errada a um método
cai em algum desses casos, mas outras exceções
são utilizadas em certos tipos de estados ou argumentos ilegais.
Por exemplo, se é passado um null onde ele
não é aceito, é lançada a NullPointerException. Já no
caso de um índice de uma seqüência que está
fora dos limites, usa-se a IndexOutOfBoundsException. Outra
exceção que vale a pena conhecer é a ConcurrentModificationexception,
lançada quando há modificação
simultânea em um objeto que deveria ser acessado apenas por uma
thread. E a última que iremos mencionar é a UnsupportedOperationException, usada
raramente, em casos que a classe não implementa alguns
métodos opcionais definidos na interface.
Existem ainda muitas outras exceções utilizadas em casos
mais específicos. É importante lembrar que o reuso
precisa estar baseado na semântica, ou seja, compatível
com a documentação da exceção, não
apenas em seu nome. Além disso, você pode fazer uma
subclasse de qualquer exceção caso queira adicionar um
pouco mais de informação sobre o problema.
Item 43: Lance exceções
apropriadas à abstração
Camadas mais altas devem tratar as exceções recebidas e,
caso apropriado, em seu lugar lançar exceções mais
adequadas em termos da abstração de alto nível.
Exemplo:
try {
//código
} catch(LowerLevelException e) {
throw new
HigherLevelException(...)
}
Uma
forma especial de tradução de exceções,
chamada encadeamento de
exceções, é apropriada em casos que a
exceção de baixo nível pode ser útil para
debugar a situação que causou a exceção.
Neste caso a exceção de baixo nível é
armazenada como um objeto dentro da nova exceção
lançada. A partir do Java 1.4 esse mecanismo é suportado
diretamente pelo Throwable, sendo
possível passar a exceção pelo construtor e
dispensando a criação do campo e de um método getCause para obter o
objeto.
Apesar da tradução de exceções ser superior
à simples propagação das mesmas,
este mecanismo não deve ser usado em excesso. O melhor é
que a classe tente tomar as ações necessárias
relativas àquela exceção, dispensando as camadas
superiores de terem que tratá-la de alguma forma.
Item 44: Documente todas as
exceções lançadas por cada método
A descrição das exceções lançadas
é uma parte importante da documentação
necessária para utilizar um método corretamente. Sempre
declare as checked exceptions individualmente, e documente as
condições exatas em que cada uma é lançada
usando a tag @throws do Javadoc.
Não utilize simplesmente uma superclasse comum às
exceções lançadas, pois isto torna obscuro o seu
uso. As runtime exceptions, apesar de não precisarem ser
declaradas, também devem estar documentadas para que o
programador esteja familiarizado com os erros mais prováveis e
possa evitá-los. Isto serve para a documentação
das
pré-condições de execução de um
método. No caso de interfaces isto é particularmente
importante, pois serve como o contrato geral e permite um comportamento
comum entre suas múltiplas implementações.
Use a tag @throws do Javadoc
para documentar cada runtime exception que o método pode
lançar, mas não utilize a palavra throws para
incluí-las na declaração do método. Assim
fica mais fácil para o programador que está usando a API
identificar visualmente quais são suas responsabilidades no uso
daquele método quando lê a documentação
gerada pelo Javadoc.
Se uma exceção é lançada por diversos
métodos da classe pela mesma razão, é
aceitável documentá-la nos comentários da
documentação da classe ao invés de individualmente
em cada método.
Item 45: Inclua informações
sobre a falha em mensagens de detalhe
O stack trace de uma exceção é sua
representação em String, resultado do método toString. Normalmente
é constituída pelo nome da classe seguido de uma mensagem
de detalhe. Isto deve retornar toda informação
possível para ajudar a pessoa a diagnosticar a causa do
problema, contendo os valores de todos os parâmetros e campos que
"contribuíram para a exceção". Porém,
lembre-se de
que o stack trace será analisado por um programador junto com o
código do método, portanto é desnecessário
incluir informações facilmente observadas lendo o
código diretamente e o conteúdo é bem mais
importante do que a forma como ele é apresentado.
Para garantir que a exceção tratará todas as
informações necessárias, seus construtores devem
obrigar
a inclusão das mesmas ao criar o objeto. Como sugerido no item
40, podem ser incluídos métodos accessors para o
programador
obter individualmente estes valores.
Item 46: Tente garantir atomicidade das
falhas
Um método cuja chamada não deu certo deve manter o objeto
no estado em que ele estava antes da chamada ao método.
Uma forma de conseguir isso é através de objetos
imutáveis (item 13). No caso de objetos mutáveis, o
melhor é checar os parâmetros antes de tentar realizar a
operação (item 23), fazendo com que as
exceções sejam lançadas antes de iniciar qualquer
modificação do objeto. No caso de não ser
possível checar os parâmetros, deve-se tentar realizar as
operações que podem falhar antes daquelas que modificam o
objeto. A terceira abordagem para atingir atomicidade das falhas
é incluir código que volte o objeto ao estado anterior
à operação. E a última forma é
efetuar as operações em uma cópia
temporária do objeto e substituir o conteúdo do objeto
original com o da cópia após completar a
operação.
No caso de objetos sendo acessados por várias threads ou de
erros (ao invés de exceptions), dificilmente será
possível atingir a atomicidade. Há também casos em
que o aumento de complexidade não compensa, e então deve
estar documentado claramente na API em que estado ficará o
objeto caso a operação falhe.
Item 47: Não ignore
exceções
É fácil simplesmente ignorar uma exceção
incluindo um bloco catch vazio. Porém, isso vai contra os
objetivos do mecanismo de exceções do Java; é como
ignorar um alarme de incêndio e ainda desligá-lo para que
mais ninguém tenha a chance de saber que algo está
pegando fogo. Portanto, no mínimo deve haver um
comentário dentro do bloco catch explicando porque a
exceção está sendo ignorada.
Bibliografia:
Bloch, Joshua. Effective Java.
Resumo por: Vanessa Sabino