A linguagem de programação Java, como muitas outras, possui ferramentas integradas para lidar com erros, ou seja, situações excepcionais (exceções) nas quais uma falha do programa é tratada por um código especial separado do algoritmo principal.
Graças às exceções, o desenvolvedor pode antecipar pontos fracos no código e evitar erros fatais durante a execução.
Portanto, o tratamento de exceções em Java é uma boa prática que melhora a confiabilidade geral do código.
O objetivo deste artigo é explorar os princípios de captura e tratamento de exceções, bem como revisar as estruturas sintáticas da linguagem destinadas a esse propósito.
Todos os exemplos deste guia foram executados no Ubuntu 22.04, instalado em um servidor em nuvem da Hostman.
Os exemplos apresentados neste guia foram executados utilizando o OpenJDK. A instalação é simples.
Primeiro, atualize a lista de repositórios disponíveis:
sudo apt update
Em seguida, solicite a lista de versões do OpenJDK disponíveis para download:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
Você verá uma pequena lista no terminal:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Usaremos a versão openjdk-21-jdk:
sudo apt install openjdk-21-jdk
Depois, você pode verificar se o Java foi instalado corretamente consultando sua versão:
java --version
A saída do terminal será semelhante a esta:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Como mostrado, a versão exata do OpenJDK é 21.0.5.
Todos os exemplos deste guia devem ser salvos em um arquivo separado com a extensão .java:
nano App.java
Em seguida, preencha o arquivo criado com um código de exemplo como este:
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Observe que o nome da classe deve coincidir com o nome do arquivo.
Depois, compile o arquivo:
javac App.java
E execute-o:
java App
O terminal exibirá o seguinte:
This text is printed to the console
Todas as exceções em Java têm um tipo específico associado ao motivo pelo qual a exceção ocorreu — o tipo particular de falha do programa.
Existem dois tipos fundamentais de exceções:
O tipo Error é considerado uma exceção apenas condicionalmente — é um erro completo que inevitavelmente causa a falha do programa.
As exceções que podem ser tratadas por código personalizado e permitem que o programa continue a execução são Checked Exceptions e Unchecked Exceptions.
Assim, erros e exceções em Java são entidades diferentes. No entanto, tanto Errors quanto Exceptions (Checked e Unchecked) são tipos com subtipos adicionais que esclarecem o motivo da falha.
Aqui está um exemplo de código que gera uma exceção em tempo de compilação:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // cria uma referência para o arquivo
Scanner scanner = new Scanner(someFile); // analisa o conteúdo do arquivo
}
}
A compilação será interrompida e você verá o seguinte erro no terminal:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Se você capturar e tratar essa exceção, o código será compilado e executado corretamente.
Aqui está outro exemplo de código que gera uma exceção apenas em tempo de execução:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // cria um array com 5 elementos
System.out.println(someArray[10]); // tenta acessar um elemento inexistente
}
}
Nenhuma exceção ocorrerá durante a compilação, mas após executar o código compilado, você verá este erro no terminal:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Isso significa que essa exceção pode ser tratada por código definido pelo usuário, permitindo que o programa continue a execução.
Por fim, aqui está um exemplo de código que causa um erro em tempo de execução:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // gera um erro de stack overflow
}
}
A compilação será bem-sucedida, mas durante a execução o terminal exibirá um StackOverflowError:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
Nesse caso, o erro não pode ser tratado; ele só pode ser corrigido no código.
Internamente, todas as exceções (e erros) em Java são representadas como um conjunto de classes, algumas das quais herdam de outras.
A classe base para todos os erros e exceções é Throwable. Duas outras classes herdam dela — Error e Exception, que servem como classes base para uma ampla gama de subclasses associadas a tipos específicos de exceção.
A classe Error descreve erros do tipo sistema, conforme mencionado anteriormente, enquanto a classe Exception descreve exceções verificadas.
Além disso, a classe RuntimeException herda de Exception e descreve exceções não verificadas.
Uma hierarquia simplificada de classes de exceção em Java pode ser representada da seguinte forma:
Cada classe de exceção inclui métodos para obter informações adicionais sobre a falha.
Você pode encontrar a classificação completa das exceções Java, incluindo as provenientes de pacotes adicionais, em um guia de referência dedicado.
Todas as exceções são tratadas usando blocos especiais try e catch, que são padrão na maioria das linguagens de programação, incluindo Java.
Dentro do bloco try, você escreve o código que pode conter um erro capaz de gerar uma exceção.
Dentro do bloco catch, você escreve o código que trata a exceção que ocorreu no bloco try previamente definido.
Por exemplo, uma estrutura try-catch pode ter a seguinte aparência:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
A saída desse código no console será:
Actually, you can't divide by zero...
Este exemplo é baseado em uma divisão ilegal por zero, envolvida em um bloco try que lança uma ArithmeticException.
No bloco catch, essa exceção é tratada imprimindo uma mensagem de erro no console.
Graças a essa estrutura, o programa pode continuar sendo executado mesmo após o erro de divisão por zero.
Diferentemente de muitas outras linguagens de programação, o Java inclui um bloco especial finally como parte do mecanismo de tratamento de exceções. Ele é sempre executado — independentemente de uma exceção ocorrer ou não.
Podemos estender a estrutura mostrada anteriormente:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
Após executar esse código, o console exibirá:
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
Para entender a necessidade prática do bloco finally, considere o exemplo a seguir:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
Em um programa com essa estrutura, a função hideLoaderUI() nunca será executada se ocorrer uma exceção.
Você poderia tentar chamar hideLoaderUI() dentro do tratamento de exceção e também fora dele:
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicado
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // duplicado
No entanto, isso resultaria em duplicação desnecessária da chamada da função. Além disso, duplicar código é considerado uma má prática.
Portanto, para garantir que hideLoaderUI() seja executado em qualquer caso sem duplicação, você pode usar um bloco finally:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// the loading indicator will be hidden in any case
hideLoaderUI();
}
O Java permite criar (lançar) exceções manualmente usando o operador especial throw:
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
Você também pode criar uma variável para a exceção antecipadamente e depois lançá-la:
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Outra palavra-chave importante, throws (observe o “s” no final), permite declarar explicitamente os tipos de exceções (na forma de nomes de classes) que um método pode lançar.
Se esse método lançar uma exceção, ela será propagada para o código chamador, que deverá tratá-la:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
A saída no console será:
Dividing by zero again? Do you even know what insanity is?
A estrutura hierárquica das exceções naturalmente permite a criação de classes de exceções personalizadas que herdam das classes base.
Graças às exceções personalizadas, o Java permite implementar caminhos de tratamento de erro específicos para a aplicação.
Assim, além das exceções padrão do Java, você pode adicionar as suas próprias.
Cada exceção personalizada, como qualquer exceção predefinida, pode ser tratada usando os blocos padrão try-catch-finally:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // chama o construtor da classe pai
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
Saída no console:
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
Este tutorial demonstrou, por meio de exemplos, por que as exceções são necessárias em Java, como elas ocorrem (incluindo como lançar uma manualmente) e como tratá-las usando as ferramentas correspondentes da linguagem.
As exceções que podem ser capturadas e tratadas são de dois tipos:
Checked Exceptions: tratadas em tempo de compilação.
Unchecked Exceptions: tratadas em tempo de execução.
Além dessas, existem erros fatais que só podem ser resolvidos reescrevendo o código:
Errors: não podem ser tratados.
Existem várias estruturas sintáticas (blocos) usadas para tratar exceções:
try: código que pode lançar uma exceção.
catch: código que trata a possível exceção.
finally: código que é sempre executado, independentemente de uma exceção ter ocorrido.
Além disso, existem palavras-chave para controlar o processo de lançamento de exceções:
throw: lança manualmente uma exceção.
throws: lista as exceções possíveis em um método declarado.
Você pode encontrar a lista completa de métodos na classe pai Exception na documentação oficial da Oracle.