11. Exceções e controle de erros

Motivação

Voltando às Contas que criamos no capítulo 6, o que aconteceria ao tentar chamar o método saca com um valor fora do limite? O sistema mostraria uma mensagem de erro, mas quem chamou o método saca não saberá que isso aconteceu.

Como avisar aquele que chamou o método de que ele não conseguiu fazer aquilo que deveria?

Em Java, os métodos dizem qual o contrato que eles devem seguir. Se, ao tentar sacar, ele não consegue fazer o que deveria, ele precisa, ao menos, avisar ao usuário que o saque não foi feito.

Veja no exemplo abaixo: estamos forçando uma Conta a ter um valor negativo, isto é, estar num estado inconsistente de acordo com a nossa modelagem.

Em sistemas de verdade, é muito comum que quem saiba tratar o erro é aquele que chamou o método e não a própria classe! Portanto, nada mais natural do que a classe sinalizar que um erro ocorreu.

A solução mais simples utilizada antigamente é a de marcar o retorno de um método como boolean e retornar true, se tudo ocorreu da maneira planejada, ou false, caso contrário:

Um novo exemplo de chamada ao método acima:

Repare que tivemos de lembrar de testar o retorno do método, mas não somos obrigados a fazer isso. Esquecer de testar o retorno desse método teria consequências drásticas: a máquina de autoatendimento poderia vir a liberar a quantia desejada de dinheiro, mesmo que o sistema não tivesse conseguido efetuar o método saca com sucesso, como no exemplo a seguir:

Mesmo invocando o método e tratando o retorno de maneira correta, o que faríamos se fosse necessário sinalizar quando o usuário passou um valor negativo como quantidade? Uma solução seria alterar o retorno de boolean para int e retornar o código do erro que ocorreu. Isso é considerado uma má prática (conhecida também como uso de “magic numbers”).

Além de você perder o retorno do método, o valor devolvido é “mágico” e só legível perante extensa documentação, além de não obrigar o programador a tratar esse retorno e, no caso de esquecer isso, seu programa continuará rodando já num estado inconsistente.

Repare o que aconteceria se fosse necessário retornar um outro valor. O exemplo abaixo mostra um caso onde, através do retorno, não será possível descobrir se ocorreu um erro ou não, pois o método retorna um cliente.

Por esses e outros motivos, utilizamos um código diferente em Java para tratar aquilo que chamamos de exceções: os casos onde acontece algo que, normalmente, não iria acontecer. O exemplo do argumento do saque inválido ou do id inválido de um cliente é uma exceção à regra.

Exceção

Uma exceção representa uma situação que normalmente não ocorre e representa algo de estranho ou inesperado no sistema.

Exercício para começar com os conceitos

Antes de resolvermos o nosso problema, vamos ver como a Java Virtual Machine age ao se deparar com situações inesperadas, como divisão por zero ou acesso a um índice da array que não existe.

Para aprendermos os conceitos básicos das exceptions do Java, teste o seguinte código você mesmo:

Repare o método main chamando metodo1 e esse, por sua vez, chamando o metodo2. Cada um desses métodos pode ter suas próprias variáveis locais, isto é: o metodo1 não enxerga as variáveis declaradas dentro do main e por aí em diante.

Como o Java (e muitas das outras linguagens) faz isso? Toda invocação de método é empilhada em uma estrutura de dados que isola a área de memória de cada um. Quando um método termina (retorna), ele volta para o método que o invocou. Ele descobre isso através da pilha de execução (stack): basta remover o marcador que está no topo da pilha:

 

 

Porém, o nosso metodo2 propositadamente possui um enorme problema: está acessando uma referência nula quando o índice for igual a 6!

Rode o código. Qual é a saída? O que isso representa? O que ela indica?

 

 

Essa saída é o conhecido rastro da pilha (stacktrace). É uma saída importantíssima para o programador – tanto que, em qualquer fórum ou lista de discussão, é comum os programadores enviarem, juntamente com a descrição do problema, essa stacktrace. Mas por que isso aconteceu?

O sistema de exceções do Java funciona da seguinte maneira: quando uma exceção é lançada (throw), a JVM entra em estado de alerta e vai ver se o método atual toma alguma precaução ao tentar executar esse trecho de código. Como podemos ver, o metodo2 não toma nenhuma medida diferente do que vimos até agora.

Como o metodo2 não está tratando esse problema, a JVM pára a execução dele anormalmente, sem esperar ele terminar, e volta um stackframe pra baixo, onde será feita nova verificação: “o metodo1 está se precavendo de um problema chamado NullPointerException?” “Não…” Volta para o main, onde também não há proteção, então a JVM morre (na verdade, quem morre é apenas a Thread corrente; se quiser saber mais sobre, há um apêndice de Threads e Programação Concorrente no final da apostila).

Obviamente, aqui estamos forçando esse caso e não faria sentido tomarmos cuidado com ele. É fácil arrumar um problema desses: basta verificar antes de chamar os métodos se a variável está com referência nula.

Porém, apenas para entender o controle de fluxo de uma Exception, vamos colocar o código que vai tentar (try) executar o bloco perigoso e, caso o problema seja do tipo NullPointerException, ele será pego (caught). Repare que é interessante que cada exceção no Java tenha um tipo… ela pode ter atributos e métodos.

Adicione um try/catch em volta do for, pegando NullPointerException. O que o código imprime?

 

 

Em vez de fazer o try em torno do for inteiro, tente apenas com o bloco de dentro do for:

Qual é a diferença?

 

Retire o try/catch e coloque ele em volta da chamada do metodo2.

 

 

Faça o mesmo, retirando o try/catch novamente e colocando em volta da chamada do metodo1. Rode os códigos, o que acontece?

 

 

Repare que, a partir do momento que uma exception foi catched (pega, tratada, handled), a execução volta ao normal a partir daquele ponto.

Exceções de Runtime mais comuns

Que tal tentar dividir um número por zero? Será que a JVM consegue fazer aquilo que nós definimos que não existe?

Tente executar o programa acima. O que acontece?

 

 

Repare que um NullPointerException poderia ser facilmente evitado com um ifque checaria se a referência é diferente de null.

Outro caso em que também ocorre tal tipo de exceção é quando um cast errado é feito (veremos mais pra frente). Em todos os casos, tais problemas provavelmente poderiam ser evitados pelo programador. É por esse motivo que o java não te obriga a dar o try/catch nessas exceptions e chamamos essas exceções de unchecked. Em outras palavras, o compilador não checa se você está tratando essas exceções.

Outro tipo de exceção: Checked Exceptions

Fica claro, com os exemplos de código acima, que não é necessário declarar que você está tentando fazer algo onde um erro possa ocorrer. Os dois exemplos, com ou sem o try/catch, compilaram e rodaram. Em um, o erro terminou o programa e, no outro, foi possível tratá-lo.

Mas não é só esse tipo de exceção que existe em Java. Um outro tipo, obriga a quem chama o método ou construtor a tratar essa exceção. Chamamos esse tipo de exceção de checked, pois o compilador checará se ela está sendo devidamente tratada, diferente das anteriores, conhecidas como unchecked.

Um exemplo interessante é o de abrir um arquivo para leitura, onde pode ocorrer o erro do arquivo não existir (veremos como trabalhar com arquivos em outro capítulo, não se preocupe com isto agora):

O código acima não compila e o compilador avisa que é necessário tratar o FileNotFoundException que pode ocorrer:

 

 

Para compilar e fazer o programa funcionar, temos duas maneiras que podemos tratar o problema. O primeiro, é tratá-lo com o try e catch do mesmo jeito que usamos no exemplo anterior, de referência nula:

A segunda forma de tratar esse erro, é delegar ele para quem chamou o nosso método, isto é, passar para a frente.

No Eclipse é bem simples fazer tanto um try/catch como um throws:

Tente digitar esse código no eclipse:

O Eclipse vai reclamar :

 

E você tem duas opções:

Add throws declaration, que vai gerar:

Surround with try/catch, que vai gerar:

No início, existe uma grande tentação de sempre passar o problema pra frente para outros o tratarem. Pode ser que faça sentido, dependendo do caso, mas não até o main, por exemplo. Acontece que quem tenta abrir um arquivo sabe como lidar com um problema na leitura. Quem chamou um método no começo do programa pode não saber ou, pior ainda, tentar abrir cinco arquivos diferentes e não saber qual deles teve um problema!

Não há uma regra para decidir em que momento do seu programa você vai tratar determinada exceção. Isso vai depender de em que ponto você tem condições de tomar uma decisão em relação àquele erro. Enquanto não for o momento, você provavelmente vai preferir delegar a responsabilidade para o método que te invocou.

Um pouco da grande família Throwable

Uma pequena parte da Família Throwable:

 

 

Mais de um erro

É possível tratar mais de um erro quase que ao mesmo tempo:

  • Com o try e catch:

  • Com o throws:

public void abre(String arquivo) throws IOException, SQLException {
// ..
}
Você pode, também, escolher tratar algumas exceções e declarar as outras no throws:

É desnecessário declarar no throws as exceptions que são unchecked, porém é permitido e às vezes, facilita a leitura e a documentação do seu código.

Lançando exceções

Lembre-se do método saca da nossa classe Conta. Ele devolve um boolean caso consiga ou não sacar:

Podemos, também, lançar uma Exception, o que é extremamente útil. Dessa maneira, resolvemos o problema de alguém poder esquecer de fazer um if no retorno de um método.

A palavra chave throw, que está no imperativo, lança uma Exception. Isto é bem diferente de throws, que está no presente do indicativo, e que apenas avisa da possibilidade daquele método lançá-la, obrigando o outro método que vá utilizar deste de se preocupar com essa exceção em questão.

No nosso caso, lança uma do tipo unchecked. RuntimeException é a exception mãe de todas as exceptions unchecked. A desvantagem, aqui, é que ela é muito genérica; quem receber esse erro não sabe dizer exatamente qual foi o problema. Podemos então usar uma Exception mais específica:

IllegalArgumentException diz um pouco mais: algo foi passado como argumento e seu método não gostou. Ela é uma Exception unchecked pois estende de RuntimeException e já faz parte da biblioteca do java. (IllegalArgumentException é a melhor escolha quando um argumento sempre é inválido como, por exemplo, números negativos, referências nulas, etc).

Para pegar esse erro, não usaremos um if/else e sim um try/catch, porque faz mais sentido já que a falta de saldo é uma exceção:

Podíamos melhorar ainda mais e passar para o construtor da IllegalArgumentException o motivo da exceção:

O método getMessage() definido na classe Throwable (mãe de todos os tipos de erros e exceptions) vai retornar a mensagem que passamos ao construtor da IllegalArgumentException.

O que colocar dentro do try?

Imagine que vamos sacar dinheiro de diversas contas:

Essa não parece uma opção boa, pois a mensagem “consegui sacar” será impressa mesmo que o catch seja acionado. Sempre que temos algo que depende da linha de cima para ser correto, devemos agrupá-lo no try:

Mas há ainda uma outra opção: imagine que, para o nosso sistema, uma falha ao sacar da conta poupança deve parar o processo de saques e nem tentar sacar da conta corrente. Para isso, agruparíamos mais ainda:

O que você vai colocar dentro do try influencia muito a execução do programa! Pense direito nas linhas que dependem uma da outra para a execução correta da sua lógica de negócios.

Criando seu próprio tipo de exceção

É bem comum criar uma própria classe de exceção para controlar melhor o uso de suas exceções. Dessa maneira, podemos passar valores específicos para ela carregar, que sejam úteis de alguma forma. Vamos criar a nossa:

Voltamos para o exemplo das Contas, vamos criar a nossa Exceção de SaldoInsuficienteException:

Em vez de lançar um IllegalArgumentException, vamos lançar nossa própria exception, com uma mensagem que dirá “Saldo Insuficiente”:

E, para testar, crie uma classe que deposite um valor e tente sacar um valor maior:

Podemos transformar essa Exception de unchecked para checked, obrigando a quem chama esse método a dar try-catch, ou throws:

Para saber mais: finally

Os blocos try e catch podem conter uma terceira cláusula chamada finally que indica o que deve ser feito após o término do bloco try ou de um catch qualquer.

É interessante colocar algo que é imprescindível de ser executado, caso o que você queria fazer tenha dado certo, ou não. O caso mais comum é o de liberar um recurso no finally, como um arquivo ou conexão com banco de dados, para que possamos ter a certeza de que aquele arquivo (ou conexão) vá ser fechado, mesmo que algo tenha falhado no decorrer do código.

No exemplo a seguir, o bloco finally será sempre executado, independentemente de tudo ocorrer bem ou de acontecer algum problema:

Há também, no Java 7, um recurso poderoso conhecido como try-with-resources, que permite utilizar a semântica do finally de uma maneira bem mais simples.

  • 12. O pacote java.lang