Tratamento de Exceção 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 Manipulação de Exceção em Java.

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 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 a exceção ocorreu e tipo de exceção.

Se ocorrer uma exceção em um método, o processo de criar o objeto de exceção e entregá-lo 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, então 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 for 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 para o 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 é encerrado e informações sobre a exceção são impressas no console.

O framework de tratamento de exceções em 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 Manipulação 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 do Java começa a processar para lidar com eles. À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 aos clientes se a senha for null. A palavra-chave throw é usada para lançar exceções ao tempo de execução para que ele as trate.
  2. throws – Quando estamos lançando uma exceção em um método e não a estamos tratando, então temos que usar a palavra-chave throws na assinatura do método para informar o programa chamador as exceções que podem ser lançadas pelo método. O método chamador pode tratar essas exceções ou propagá-las para seu método chamador usando a palavra-chave throws. Podemos fornecer múltiplas exceções na cláusula throws, e ela pode ser usada com o método main() também.
  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 tratar as exceções. Podemos ter múltiplos blocos catch com um bloco try. O bloco try-catch também pode ser aninhado. 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 Tratamento de Exceção

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 tratando 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 método printStackTrace() é um dos métodos úteis na classe Exception para fins de depuração.

Este código irá 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 declaraçã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 de forma semelhante às instruções if-else.
  • Podemos ter apenas um bloco finally com uma instrução try-catch.

Hierarquia de Exceções em Java

Conforme 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 que estão fora do escopo da aplicação, e não é possível antecipar e recuperar deles. Por exemplo, falha de hardware, falha na máquina virtual Java (JVM) ou erro de falta de memória. É por isso que 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 recuperar. Por exemplo, FileNotFoundException. Devemos capturar essa 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 estivermos lançando uma Exceção Verificada, devemos capturá-la no mesmo método, ou precisamos 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 programação inadequada. Por exemplo, tentar recuperar um elemento de uma matriz. Devemos verificar o comprimento da matriz antes de tentar recuperar o elemento, caso contrário, pode lançar ArrayIndexOutOfBoundsException em tempo de execução. RuntimeException é a classe pai de todas as Exceções de Tempo de Execução. Se estivermos throwando qualquer Exceção de Tempo de Execução em um método, não é necessário especificá-las na cláusula throws da assinatura do método. Exceções de tempo de execução podem ser evitadas com uma programação mais cuidadosa.

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 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 em formato String, a String retornada contém o nome da classe Throwable e a 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á catchando muitas exceções em um único bloco try, você notará que o código do bloco catch consiste principalmente em código redundante para registrar o erro. No Java 7, uma das características foi um bloco catch melhorado 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 recebemos exceções de 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 declaração try e usá-lo dentro do bloco try-catch. Quando a execução sai do bloco try-catch, o ambiente de tempo 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, digamos que escrevemos um método para processar apenas arquivos de texto, então podemos fornecer ao chamador o código de erro apropriado quando 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;
	}
}

Depois, 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 lançada, ela deva ser tratada no método ou retornada ao programa chamador. Se estendermos RuntimeException, não há necessidade de especificá-lo na cláusula throws.

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

Melhores Práticas para 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 o Java possui várias 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 a causa raiz da exceção facilmente e as processe. Isso facilita a depuração e ajuda as aplicações cliente a lidar 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() mencionado acima, se passarmos o argumento null para esse método, receberemos 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)

Ao depurar, teremos que observar cuidadosamente o rastreamento da pilha para identificar a localização real da exceção. Se alterarmos nossa lógica de implementação para verificar essas exceções mais cedo, 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, o rastreamento da pilha 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)
  • Captura Tardia – Como o Java obriga a lidar com a exceção verificada ou declará-la na assinatura do método, às vezes os desenvolvedores tendem a capturar 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 capturar exceções apenas quando podemos lidar com elas adequadamente. Por exemplo, no método acima, estou lançando exceções de volta para o método chamador para lidar com ela. O mesmo método poderia ser usado por outras aplicações que talvez queiram processar a exceção de maneira diferente. Ao implementar qualquer recurso, devemos sempre lançar exceções de volta para o chamador e permitir que eles decidam como lidar com ela.
  • Fechamento de 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 tempo de execução do Java o feche para você.
  • Registrando Exceções – Sempre devemos registrar mensagens de exceção e ao lançar exceções fornecer uma mensagem clara para que o chamador saiba facilmente por que a exceção ocorreu. 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 catch único para várias exceções – Na maioria das vezes, registramos 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.
  • Usando 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 várias 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 pelo nome em si 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 parado 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 os blocos try (e try-with-resources), catch e finally.

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