8.10. Exceções

Já se deve ter deparado com situações em que a execução de um programa termina abruptamente e uma mensagem é escrita na consola. Esta mensagem é muito útil para quem a sabe decifrar, pois a partir dela conseguimos perceber qual o tipo de problema ocorrido e onde, no código do programa, foi detetado.

Se executarmos o seguinte programa,

e respondermos à sua solicitação escrevendo os carateres 31.w2, por exemplo, o programa terminará abruptamente com a seguinte mensagem:

Este programa não é robusto em relação à introdução errada de dados, pois o método nextDouble da classe Scanner só tolera sequências de carateres que representam double, como se pode ver na API dessa classe:

O mesmo se aplica aos métodos nextInt, nextFloat, etc.

Podemos sempre evitar este problema recorrendo a outros métodos da classe Scanner (por exemplo hasNext, hasNextInt, hasNextDouble) que permitem observar o canal de entrada antes de decidir ler o valor que lá está.

 

Outra abordagem possível é permitir que o erro ocorra e reagir de forma adequada, sem deixar que o programa termine abruptamente. Isso é feito através do tratamento das exceções, como iremos ver.

 

8.10.1. O que são exceções

“Exception” ou exceção, é uma abreviatura para “Exceptional Event”, que é um evento que ocorre durante a execução de um programa e que interrompe o seu fluxo normal.

Quando, durante a execução do programa, é detetado um erro:

Na figura da página anterior pode ver a pilha de chamadas que é apresentada na consola quando a exceção ocorre.

Na biblioteca do Java existem várias classes que representam exceções que se aplicam a uma série de situações mais ou menos comuns em execuções de programas.

Como exemplos bastante conhecidos, temos NullPointerException, que é lançada sempre que um programa tenta usar null numa situação em que é requerido um objeto (por exemplo, invocar um método sobre uma referência que é null), ArrayIndexOutOfBoundsException, que é lançada sempre que se tenta aceder um array usando um índice ilegal, ArithmeticException, que é lançada quando alguma condição aritmética excecional ocorre (por exemplo, divisão por zero), etc.

 

Existem várias alternativas para lidar com as exceções.

 

8.10.2. Lidar com uma exceção

Por norma, um método que pode lançar uma exceção tem de assumir o tratamento dessa mesma exceção. Existem duas formas de um método tratar uma exceção:

  1. Apanhar a exceção, usando a instrução try-catch que permite tratar a exceção da forma que se ache adequada;
  2. Reencaminhar a exceção para o método que o invocou, acrescentando a cláusula throws na assinatura do método. Também se chama a isto especificar a exceção.

Se todos os métodos na pilha de chamadas reencaminharem a exceção, incluindo o main onde tudo começou, o programa termina abruptamente indicando, como no exemplo anterior, qual a exceção ocorrida e a pilha de chamadas.

Vamos agora estudar estas duas formas de tratamento de exceções.

 

1. Apanhar uma exceção (try-catch)

A ideia é "isolar" as instruções que sabemos que podem provocar exceções, e definir ações alternativas a serem executadas como resposta a cada tipo possível de exceção.

Fazemos isso através de um bloco de instruções try-catch, composto por:

Um bloco catch recebe a exceção como parâmetro.

 

Em cada um dos blocos catch podemos ter, em alternativa:

a. código que vai ser executado para resolver o erro (por exemplo, informando o utilizador de que aconteceu algo errado durante a execução do programa);

b. propagação da exceção, eventualmente de forma diferente (outro tipo de exceção), para o método que invocou o método corrente.

 

1.a. Resolver a exceção

Como exemplo da alternativa a., em reação à exceção InputMismatchException que pode ser provocada pela execução do método nextDouble, podemos “apanhar” essa exceção e informar o utilizador que o programa terminou sem o resultado esperado, porque um dos valores que o programa estava à espera de receber não é do tipo double.

Se executarmos esta nova versão do programa:

e voltarmos a introduzir os carateres 31.w2, a mensagem será:

 

1.b. Lançar uma exceção (throw)

Podemos querer substituir a exceção que foi lançada por outra que consideremos mais adequada. Podemos usar uma das exceções já definidas na biblioteca do Java, ou um novo tipo de exceção por nós criado.

Quando uma exceção é criada, é possível associar-lhe uma mensagem específica através de um parâmetro do construtor. Essa mensagem pode depois ser obtida através da invocação do método getMessage da exceção. Esta mensagem aparece na primeira linha da pilha de mensagens que é mostrada ao utilizador quando o programa termina devido a uma exceção não tratada.

 

Na seguinte versão do nosso programa, o bloco catch que trata a exceção InputMismatchException lança uma nova exceção, do tipo NumberFormatException, e associa-lhe uma mensagem elucidativa.

Se executarmos esta nova versão do programa:

e voltarmos a introduzir os carateres 31.w2, aparecerá o seguinte na consola:

 

As soluções apresentadas acima resolvem o problema quando algo corre mal com a aquisição do valor de n, contudo não resolvem todos os problemas.

Por exemplo, o que acontece quando o número n é um número não positivo (relembre que o logaritmo é indefinido para valores não positivos)? Apesar de o programa não dar erro nem lançar uma exceção (devolve NAN ou Infinity), pode-se interpretar esta situação como um comportamento excecional e que, portanto, deve dar origem a uma exceção.

No exemplo seguinte, o comportamento excecional é sinalizado lançando uma exceção do tipo InputMismatchException, que depois será apanhada no bloco catch correspondente.

Se executarmos este programa e introduzirmos o valor -1, a mensagem será:

Como já foi referido, quando uma exceção é criada, é possível associar-lhe uma mensagem específica. Essa mensagem pode depois ser obtida através da invocação do método getMessage da exceção.

Como sabemos que o método nextDouble dá origem a uma exceção sem qualquer mensagem associada quando o formato do input não é o esperado, então, se compararmos o resultado do getMessage com null, ficamos a saber que é esse o caso.

O ramo else do if cobre as situações em que o resultado não é null , como por exemplo a exceção que é lançada quando n <= MINIMO.

 

2. Reencaminhar uma exceção para outro método (throws)

Se num método m não sabemos como devemos tratar uma exceção, porque desconhecemos as especificidades do contexto em que foi invocado, devemos deixar esse tratamento para o método que invocou m.

Quando não queremos apanhar uma exceção num método m,

Quando um método gera uma exceção que não é tratada internamente, a sua execução termina de imediato.

No exemplo seguinte, o método lerDeUmFicheiro, que lê um valor a partir de um ficheiro, não trata as exceções do tipo FileNotFoundException e, por isso, tem na sua assinatura a declaração throws FileNotFoundException.

No caso de ocorrer uma exceção provocada por erros de acesso ao ficheiro, ela é passada para o método que invocou este método e assim sucessivamente, até encontrar um método que trate a exceção ou, em última instância, até chegar ao método main. Neste caso, se o main não trata a exceção (também tem throws), o programa termina a sua execução indicando uma mensagem de erro.

Se executarmos este programa sem que exista acessível um ficheiro de nome inteiros.txt, a mensagem obtida será:

Se existir um ficheiro de nome inteiros.txt, o valor que for lido determinará a reação do programa, como já vimos nos exemplos anteriores.

 

8.10.3. Exceções checked e unchecked

Relembre o primeiro programa apresentado neste capítulo:

Embora o método nextDouble da classe Scanner possa provocar uma exceção InputMismatchException (além de outras), o compilador não nos obrigou nem a tratá-la nem a declarar no método main que isso podia acontecer (com throws InputMismatchException).

Em contraste, o seguinte programa tem um erro de compilação:

O erro de compilação é:

Para resolvermos este erro temos que tratar a exceção com um bloco try-catch ou acrescentar throws FileNotFoundException à assinatura do método main.

 

O que justifica esta diferença?

Em Java existem dois tipos de exceções, no que diz respeito ao seu tratamento:

Claramente as exceções do tipo InputMismatchException são unchecked e as do tipo FileNotFoundException são checked.

A figura abaixo mostra parte da hierarquia de exceções do Java. Todas as RunTimeException são unchecked.

 

 

A ideia subjacente à existência destes dois tipos de exceções é a de que as exceções checked são aquelas das quais um método cliente tem hipótese de recuperar e as unchecked aquelas que um método cliente não consegue ou não deve tratar, por poderem configurar situações de programação deficiente.

Por exemplo, quando um pedaço de código pode lançar uma NullPointerException ou uma ArrayIndexOutOfBoundsException, o melhor que temos a fazer é corrigir o nosso código em vez de tratar essas exceções.

 

8.10.4. Try-with-resources em Java

Voltando ao exemplo do método lerDeUmFicheiro, da classe ExemploExcecoes5, o que acontece ao Scanner quando o método lerDeUmFicheiro termina lançando a exceção InputMismatch causada pelo lançamento dessa mesma exceção pelo método nextDouble?

Como a execução termina abruptamente, as instruções sc.close() e return n não são executadas. Nesse caso o Scanner fica “aberto”. É necessário fechá-lo, isto é libertar este recurso para outras execuções.

De modo a simplificar a tarefa dos programadores, a instrução try pode ser reforçada com a indicação dos recursos que deverão ser fechados de forma automática no final do bloco (seja lançada uma exceção ou não):

    public static double lerDeUmFicheiro(String nomeFicheiro) 
    		throws FileNotFoundException {

        try (Scanner sc = new Scanner(new File(nomeFicheiro))){
    
            double n = sc.nextDouble();
     
            return n; 
        }
    }        

Repare que assim já não precisa de invocar explicitamente o método close() do Scanner.

Num mesmo bloco try podem ser iniciados vários recursos. A indicação desses vários recursos é feita através de separação com ; dentro do parêntesis que se segue à palavra reservada try.

 

8.10.5. O bloco finally

O bloco finally é usado sempre que se pretende garantir que um dado bloco de instruções é executado mesmo que seja lançada uma exceção. Antes do Java 7, o papel de fecho dos recursos cabia ao programador e as instruções necessárias eram realizadas dentro de um bloco finally.

A partir de então, essa tarefa é automática com o try-with-resources.

No entanto, há situações onde se justifica a utilização do bloco finally como, por exemplo, para finalizar tarefas importantes de escrita (por exemplo em bases de dados), ou eliminação de ficheiros que foram criados como meios auxiliares ou colocar estruturas num estado específico.

 


 

Anterior: 8.9. Ficheiros de texto - leitura e escrita

Seguinte: 9. Arrays