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,
ximport java.util.Scanner;
public class ExemploExcecoes1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Introduza um valor numerico");
double n = sc.nextDouble();
System.out.println("log_2 (" + n + ") = " + Math.log(n));
sc.close();
}
}
e respondermos à sua solicitação escrevendo os carateres 31.w2
, por exemplo, o programa terminará abruptamente com a seguinte mensagem:
xxxxxxxxxx
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextDouble(Scanner.java:2564)
at ExemploExcecoes1.main(ExemploExcecoes1.java:10)
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.
“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:
uma exceção é lançada (throwing an exception), ou seja, é criado um objeto Java que contém as informações relevantes sobre o erro ocorrido:
- o seu tipo e
- o estado do programa quando o erro ocorreu.
o sistema tenta lidar com o problema
- recorre à lista ordenada de métodos que foram invocados para chegar ao método onde o erro ocorreu (pilha de chamadas) e
- vê se algum desses métodos é capaz de tratar a exceção (handle the exception).
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.
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:
try-catch
que permite tratar a exceção da forma que se ache adequada;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.
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
try
:
- neste bloco colocamos as instruções que queremos executar e que sabemos que podem levantar exceções;
um ou mais blocos
catch
a serem executados quando alguma das instruções no blocotry
provoca o lançamento de uma exceção.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.
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:
xxxxxxxxxx
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExemploExcecoes2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Introduza um valor numerico");
try {
double n = sc.nextDouble();
System.out.println("log_2 (" + n + ") = " + Math.log(n));
} catch(InputMismatchException e) {
System.out.println("Ocorreu um erro. Formato errado");
}
sc.close();
}
}
e voltarmos a introduzir os carateres 31.w2
, a mensagem será:
xxxxxxxxxx
Ocorreu um erro. Formato errado
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.
- A criação de uma exceção é feita, como com qualquer outro objeto, com a instrução
new
.- O construtor recebe uma string, que pode depois ser acedida através da invocação do método
getMessage
da exceção.- O lançamento de uma exceção é feito usando a instrução
throw
.
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:
xxxxxxxxxx
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExemploExcecoes3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Introduza um valor numerico");
try {
double n = sc.nextDouble();
System.out.println("log_2 (" + n + ") = " + Math.log(n));
} catch(InputMismatchException e) {
throw new NumberFormatException("Formato do input invalido");
}
sc.close();
}
}
e voltarmos a introduzir os carateres 31.w2
, aparecerá o seguinte na consola:
xxxxxxxxxx
Exception in thread "main" java.lang.NumberFormatException: Formato do input invalido
at ExemploExcecoes3.main(ExemploExcecoes3.java:16)
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.
xxxxxxxxxx
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExemploExcecoes4 {
public static final double MINIMO = 0.001;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Introduza um valor numerico");
try {
double n = sc.nextDouble();
if(n <= MINIMO) {
throw new InputMismatchException("Ocorreu um erro.\n" +
"O argumento não é positivo!");
}
System.out.println("log_2 (" + n + ") = " + Math.log(n));
} catch(InputMismatchException e) {
if (e.getMessage() == null) {
System.out.println("Ocorreu um erro. Formato errado");
} else {
System.out.println(e.getMessage());
}
}
sc.close();
}
}
Se executarmos este programa e introduzirmos o valor -1
, a mensagem será:
xxxxxxxxxx
Ocorreu um erro.
O argumento não é positivo!
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
.
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
,
- temos que “anunciar” que a execução de
m
pode levar ao seu lançamento (especificar a exceção);- Para isso, acrescentamos à assinatura do método a palavra
throws
(notar o “s” no final da palavra) seguida do nome das exceções potencialmente geradas e não tratadas.
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.
xxxxxxxxxx
import java.io.File;
import java.io.FileNotFoundException;
import java.util.InputMismatchException;
import java.util.Scanner;
public class ExemploExcecoes5 {
public static final double MINIMO = 0.001;
public static void main (String[] args) throws FileNotFoundException{
try {
double n = lerDeUmFicheiro("inteiros.txt");
if(n <= MINIMO) {
throw new InputMismatchException("Ocorreu um erro.\n" +
"O argumento nao eh positivo!");
}
System.out.println("log_2 (" + n + ") = " + Math.log(n));
} catch (InputMismatchException e) {
if(e.getMessage() == null) {
System.out.println("Ocorreu um erro. Formato errado.");
} else {
System.out.println(e.getMessage());
}
}
}
public static double lerDeUmFicheiro(String nomeFicheiro)
throws FileNotFoundException {
Scanner sc = new Scanner(new File(nomeFicheiro));
double n = sc.nextDouble();
sc.close();
return n;
}
}
Se executarmos este programa sem que exista acessível um ficheiro de nome inteiros.txt
, a mensagem obtida será:
xxxxxxxxxx
Exception in thread "main" java.io.FileNotFoundException: inteiros.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:211)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:153)
at java.base/java.util.Scanner.<init>(Scanner.java:639)
at ExemploExcecoes5.lerDeUmFicheiro(ExemploExcecoes5.java:34)
at ExemploExcecoes5.main(ExemploExcecoes5.java:13)
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.
Relembre o primeiro programa apresentado neste capítulo:
xxxxxxxxxx
import java.util.Scanner;
public class ExemploExcecoes1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Introduza um valor numerico");
double n = sc.nextDouble();
System.out.println("log_2 (" + n + ") = " + Math.log(n));
sc.close();
}
}
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:
xxxxxxxxxx
public class ExemploExcecoes6 {
public static void main (String[] args) {
Scanner sc = new Scanner(new File("inteiros.txt"));
double n = sc.nextDouble();
System.out.println("log_2 (" + n + ") = " + Math.log(n));
sc.close();
}
}
O erro de compilação é:
xxxxxxxxxx
Unhandled exception type FileNotFoundException
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:
Checked: são as exceções que são verificadas em tempo de compilação. Se alguma parte do código de um método pode dar origem a uma exceção checked, então o método tem que:
- tratar a exceção através de um bloco
try-catch
ou- anunciar que essa exceção pode acontecer, adicionando à sua assinatura a palavra
throws
seguida do nome da exceção.Unchecked: são as exceções que não são verificadas em tempo de compilação.
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.
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
.
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