Tratamento de Exceções em Java

Introdução

Uma exceção é um evento de erro que pode ocorrer durante a execução de um programa e interromper seu fluxo normal. O Java fornece uma maneira robusta e orientada a objetos de lidar com cenários de exceção conhecida como Tratamento de Exceções em Java.

As exceções em Java podem surgir de diferentes tipos de situações, como dados incorretos inseridos pelo usuário, falha de hardware, falha na conexão de rede ou um servidor de banco de dados que está fora do ar. O código que especifica o que fazer em cenários de exceção específicos é chamado de tratamento de exceção.

Lançando e Capturando Exceções

O Java cria um objeto de exceção quando ocorre um erro durante a execução de uma instrução. O objeto de exceção contém muitas informações de depuração, como hierarquia de método, número da linha onde ocorreu a exceção e tipo de exceção.

Se ocorrer uma exceção em um método, o processo de criação do objeto de exceção e sua entrega ao ambiente de execução é chamado “lançar a exceção”. O fluxo normal do programa é interrompido e o Ambiente de Execução Java (JRE) tenta encontrar o manipulador para a exceção. O manipulador de exceção é o bloco de código que pode processar o objeto de exceção.

  • A lógica para encontrar o manipulador de exceção começa com a busca no método onde o erro ocorreu.
  • Se nenhum manipulador apropriado for encontrado, ele se moverá para o método chamador.
  • E assim por diante.

Portanto, se a pilha de chamadas do método for A->B->C e uma exceção é gerada no método C, então a busca pelo manipulador apropriado se moverá de C->B->A.

Se um manipulador de exceção apropriado for encontrado, o objeto de exceção é passado ao manipulador para processá-lo. Diz-se que o manipulador está “capturando a exceção”. Se não houver um manipulador de exceção apropriado, o programa será encerrado e imprimirá informações sobre a exceção no console.

O framework de tratamento de exceções do Java é usado apenas para lidar com erros em tempo de execução. Os erros em tempo de compilação devem ser corrigidos pelo desenvolvedor que escreve o código, caso contrário, o programa não será executado.

Palavras-chave de Tratamento de Exceção em Java

Java fornece palavras-chave específicas para fins de tratamento de exceções.

  1. throw – Sabemos que se ocorrer um erro, um objeto de exceção é criado e então o tempo de execução Java começa a processá-lo. Às vezes, podemos querer gerar exceções explicitamente em nosso código. Por exemplo, em um programa de autenticação de usuário, devemos lançar exceções para os clientes se a senha for null. A palavra-chave throw é usada para lançar exceções para o tempo de execução para manipulá-la.
  2. throws – Quando estamos lançando uma exceção em um método e não a manipulando, então temos que usar a palavra-chave throws na assinatura do método para informar ao programa chamador as exceções que podem ser lançadas pelo método. O método chamador pode manipular essas exceções ou propagá-las para o seu método chamador usando a palavra-chave throws. Podemos fornecer várias exceções na cláusula throws, e ela pode ser usada também com o método main().
  3. try-catch – Usamos o bloco try-catch para tratamento de exceções em nosso código. try é o início do bloco e catch está no final do bloco try para manipular as exceções. Podemos ter vários blocos catch com um bloco try. O bloco try-catch pode ser aninhado também. O bloco catch requer um parâmetro que deve ser do tipo Exception.
  4. finalmente – o bloco finally é opcional e pode ser usado apenas com um bloco try-catch. Como a exceção interrompe o processo de execução, podemos ter alguns recursos abertos que não serão fechados, então podemos usar o bloco finally. O bloco finally sempre é executado, quer ocorra uma exceção ou não.

Um Exemplo de Manipulação de Exceções

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • O método testException() está lançando exceções usando a palavra-chave throw. A assinatura do método usa a palavra-chave throws para informar ao chamador o tipo de exceções que ele pode lançar.
  • No método main(), estou manipulando exceções usando o bloco try-catch no método main(). Quando não estou tratando, estou propagando para tempo de execução com a cláusula throws no método main().
  • O testException(-10) nunca é executado por causa da exceção e então o bloco finally é executado.

O printStackTrace() é um dos métodos úteis na classe Exception para fins de depuração.

Este código produzirá o seguinte:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

Alguns pontos importantes a serem observados:

  • Não podemos ter uma cláusula catch ou finally sem uma instrução try.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • Não podemos escrever nenhum código entre blocos try-catch-finally.
  • Podemos ter vários blocos catch com uma única instrução try.
  • Os blocos try-catch podem ser aninhados, assim como as instruções if-else.
  • Podemos ter apenas um bloco finally com uma instrução try-catch.

Hierarquia de Exceções em Java

Como mencionado anteriormente, quando uma exceção é gerada, um objeto de exceção é criado. As exceções em Java são hierárquicas, e a herança é usada para categorizar diferentes tipos de exceções. Throwable é a classe pai da hierarquia de exceções em Java e possui dois objetos filhos – Error e Exception. As Exceptions são ainda divididas em Exceptions Verificadas e Exceptions de Tempo de Execução.

  1. Erros: Errors são cenários excepcionais fora do escopo da aplicação, e não é possível prever e se recuperar deles. Por exemplo, falha de hardware, falha na máquina virtual Java (JVM) ou erro de falta de memória. Por isso, temos uma hierarquia separada de Errors e não devemos tentar lidar com essas situações. Alguns dos Errors comuns são OutOfMemoryError e StackOverflowError.
  2. Exceções Verificadas: Exceções verificadas são cenários excepcionais que podemos antecipar em um programa e tentar nos recuperar deles. Por exemplo, FileNotFoundException. Devemos capturar esta exceção e fornecer uma mensagem útil ao usuário e registrá-la adequadamente para fins de depuração. A Exception é a classe pai de todas as exceções verificadas. Se estamos lançando uma exceção verificada, devemos capturá-la no mesmo método, ou temos que propagá-la para o chamador usando a palavra-chave throws.
  3. Exceção de Tempo de Execução: Exceções de tempo de execução são causadas por má programação. Por exemplo, tentar recuperar um elemento de uma matriz. Devemos verificar o comprimento da matriz primeiro antes de tentar recuperar o elemento, caso contrário, pode lançar ArrayIndexOutOfBoundException em tempo de execução. RuntimeException é a classe pai de todas as exceções de tempo de execução. Se estamos lançando alguma exceção de tempo de execução em um método, não é necessário especificá-las na cláusula de assinatura do método throws. Exceções de tempo de execução podem ser evitadas com uma melhor programação.

Alguns métodos úteis das Classes de Exceção

Java Exception e todas as suas subclasses não fornecem métodos específicos, e todos os métodos são definidos na classe base – Throwable. As classes de Exception são criadas para especificar diferentes tipos de cenários de Exception para que possamos identificar facilmente a causa raiz e lidar com a Exception de acordo com o seu tipo. A classe Throwable implementa a interface Serializable para interoperabilidade.

Alguns dos métodos úteis da classe Throwable são:

  1. public String getMessage() – Este método retorna a mensagem String de Throwable e a mensagem pode ser fornecida ao criar a exceção por meio de seu construtor.
  2. public String getLocalizedMessage() – Este método é fornecido para que subclasses possam substituí-lo para fornecer uma mensagem específica para a localidade ao programa chamador. A implementação da classe Throwable deste método usa o método getMessage() para retornar a mensagem de exceção.
  3. public synchronized Throwable getCause() – Este método retorna a causa da exceção ou null se a causa for desconhecida.
  4. public String toString() – Este método retorna as informações sobre Throwable no formato String, a String retornada contém o nome da classe Throwable e mensagem localizada.
  5. public void printStackTrace() – Este método imprime as informações da pilha de execução no fluxo de erro padrão. Este método é sobrecarregado e podemos passar PrintStream ou PrintWriter como argumento para escrever as informações da pilha de execução no arquivo ou fluxo.

Java 7 Gerenciamento Automático de Recursos e melhorias no bloco catch

Se você está capturando muitas exceções em um único bloco try, você notará que o código no bloco catch consiste principalmente em código redundante para registrar o erro. No Java 7, uma das características foi um aprimoramento no bloco catch onde podemos capturar várias exceções em um único bloco catch. Aqui está um exemplo do bloco catch com esse recurso:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

Há algumas restrições, como o objeto de exceção ser final e não podermos modificá-lo dentro do bloco catch. Leia a análise completa em Melhorias no Bloco Catch do Java 7.

Na maioria das vezes, usamos o bloco finally apenas para fechar os recursos. Às vezes, esquecemos de fechá-los e obtemos exceções em tempo de execução quando os recursos estão esgotados. Essas exceções são difíceis de depurar, e pode ser necessário examinar cada local onde estamos usando esse recurso para garantir que estamos fechando-o. No Java 7, uma das melhorias foi o try-with-resources, onde podemos criar um recurso na própria instrução try e usá-lo dentro do bloco try-catch. Quando a execução sai do bloco try-catch, o ambiente de execução fecha automaticamente esses recursos. Aqui está um exemplo do bloco try-catch com essa melhoria:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

O Java fornece muitas classes de exceção para usarmos, mas às vezes podemos precisar criar nossas próprias classes de exceção personalizadas. Por exemplo, para notificar o chamador sobre um tipo específico de exceção com a mensagem apropriada. Podemos ter campos personalizados para rastreamento, como códigos de erro. Por exemplo, suponha que escrevemos um método para processar apenas arquivos de texto, podemos fornecer ao chamador o código de erro apropriado quando algum outro tipo de arquivo é enviado como entrada.

Primeiro, crie MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

Em seguida, crie um CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

Podemos ter um método separado para processar diferentes tipos de códigos de erro que obtemos de diferentes métodos. Alguns deles são consumidos porque talvez não queiramos notificar o usuário disso, ou alguns deles serão lançados de volta para notificar o usuário do problema.

Aqui estou estendendo Exception para que, sempre que essa exceção for gerada, ela precise ser tratada no método ou retornada ao programa chamador. Se estendermos RuntimeException, não há necessidade de especificá-la na cláusula throws.

Esta foi uma decisão de design. O uso de Checked Exceptions tem a vantagem de ajudar os desenvolvedores a entender quais exceções podem ser esperadas e tomar as medidas apropriadas para lidar com elas.

Práticas Recomendadas para o Tratamento de Exceções em Java

  • Use Exceções Específicas – As classes base da hierarquia de exceções não fornecem informações úteis, é por isso que o Java possui tantas classes de exceção, como IOException com sub-classes adicionais como FileNotFoundException, EOFException, etc. Devemos sempre lançar e capturar classes de exceção específicas para que o chamador saiba facilmente a causa raiz da exceção e as processe. Isso torna a depuração mais fácil e ajuda as aplicações cliente a lidarem com exceções adequadamente.
  • Lance Cedo ou Falhe Rapidamente – Devemos tentar lançar exceções o mais cedo possível. Considere o método processFile() acima, se passarmos o argumento null para este método, obteremos a seguinte exceção:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

Durante a depuração, teremos que observar cuidadosamente a pilha de execução para identificar a localização real da exceção. Se alterarmos nossa lógica de implementação para verificar essas exceções antecipadamente, como abaixo:

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... processamento adicional
}

Então, a pilha de execução da exceção indicará onde a exceção ocorreu com uma mensagem clara:

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Capturar Tardio – Como o Java exige que lidemos com a exceção verificada ou a declaremos na assinatura do método, às vezes os desenvolvedores têm a tendência de catch a exceção e registrar o erro. Mas essa prática é prejudicial porque o programa chamador não recebe notificação da exceção. Devemos catch exceções apenas quando podemos lidar com elas adequadamente. Por exemplo, no método acima, estou throw exceções de volta para o método chamador para que ele as manipule. O mesmo método pode ser usado por outras aplicações que desejam processar a exceção de maneira diferente. Ao implementar qualquer recurso, devemos sempre throw exceções de volta para o chamador e deixar que ele decida como lidar com ela.
  • Fechar Recursos – Como as exceções interrompem o processamento do programa, devemos fechar todos os recursos no bloco finally ou usar o aprimoramento try-with-resources do Java 7 para permitir que o Java runtime o feche para você.
  • Registo de Exceções – Devemos sempre registar mensagens de exceção e ao lançar exceções fornecer uma mensagem clara para que o chamador saiba facilmente por que ocorreu a exceção. Devemos sempre evitar um bloco catch vazio que apenas consome a exceção e não fornece detalhes significativos da exceção para depuração.
  • Bloco de captura único para várias exceções – Na maioria das vezes registamos detalhes da exceção e fornecemos uma mensagem ao usuário, neste caso, devemos usar o recurso do Java 7 para lidar com várias exceções em um único bloco catch. Esta abordagem reduzirá o tamanho do nosso código e também parecerá mais limpa.
  • Utilizando Exceções Personalizadas – É sempre melhor definir uma estratégia de tratamento de exceção no momento do design e, em vez de lançar e capturar múltiplas exceções, podemos criar uma exceção personalizada com um código de erro, e o programa chamador pode lidar com esses códigos de erro. Também é uma boa ideia criar um método utilitário para processar diferentes códigos de erro e usá-los.
  • Convenções de Nomenclatura e Empacotamento – Ao criar sua exceção personalizada, certifique-se de que ela termine com Exception para que fique claro a partir do próprio nome que é uma classe de exceção. Além disso, certifique-se de empacotá-las como é feito no Kit de Desenvolvimento Java (JDK). Por exemplo, IOException é a exceção base para todas as operações de IO.
  • Use Exceções com Cautela – As exceções são custosas e às vezes não é necessário lançar exceções de todo, podemos retornar uma variável booleana para o programa chamador indicar se uma operação foi bem-sucedida ou não. Isso é útil quando a operação é opcional e você não quer que seu programa fique preso por falha. Por exemplo, ao atualizar as cotações de estoque no banco de dados a partir de um serviço da web de terceiros, podemos querer evitar lançar exceções se a conexão falhar.
  • Documente as Exceções Lançadas – Use Javadoc @throws para especificar claramente as exceções lançadas pelo método. Isso é muito útil quando você está fornecendo uma interface para outras aplicações usarem.

Conclusão

Neste artigo, você aprendeu sobre o tratamento de exceções em Java. Você aprendeu sobre throw e throws. Você também aprendeu sobre blocos try (e try-with-resources), catch e finally.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java