Trabalhando com Exceções em Java

Exceções em Java

A linguagem e a plataforma Java abraçaram o conceito de exceções orientadas a ojetos desde o ínício.

A linguagem Java introduziu pela primeira vez o conceito de Exceção verificada ( Checked Exception). A base para isto é que certas condições são tão importantes que o programador não deve se escusar de lidar com o problema imediatamente. Normalmente este tipo de situação existe quando o programa tem que interagir com o ambiente em que executa, forade máquina virtual, por exemplo, com o sistema de arquivos ou a rede.

Exceções em Java

Em Java exceções são representadas por uma hierarquia particular de objetos cuja classe raiz é a classe Throwable.

chamadas

Tipos de Exceção

Em Java, existem três categorias de exceções: Erro, Falha e Exceção de Contingência representadas respetivamente pelas classes: Error, RuntimeException e Exception. Todas, derivadas de Throwable.

A hierarquia de exceções em Java não visa criar implementações ligeiramente diferentes da mesma coisa e sim diferenciar categorias diferentes de exceções. Para cada tipo de exceção existe uma interpretação especial feita pelo compilador que se reflete na forma como o programador tem que lidar com elas.

hierarquia

Erros

Erros são exceções muito graves, tão graves que representam problemas que o programa não tem como resolver. São erros todas as classes que descendem diretamente de Error.

É importante que os erros sejam reportados e que se saiba que aconteceram, mas o programa não tem o que fazer para resolver o problema que eles apontam. Erros indicam que alguma coisa está realmente muito errada no funcionamento do código ou no ambiente de execução. Exemplos de erros são OutOfMemoryError que é lançada quando o programa precisa de mais memória, mas ela não está disponível, e StackOverflowError que acontece quando a pilha estoura, por exemplo, quando um método se chama a si mesmo sem nunca retornar.

public int stackOverFlow(int a){
    return stackOverFlow(a);
}

O lançamento de Erros normalmente levam ao crash da JVM.

Falhas

Falhas são exceções que a aplicação causa e pode resolver. A aplicação pode resolver, mas não é obrigada a fazê-lo. São falhas todas as classes que descendem diretamente de RuntimeException.

Se a aplicação nunca apanhar este tipo de exceção, tudo bem, a JVM irá capturá-la. Mas provavelmente a sua aplicação não mais funcionará corretamente. Exemplos de falhas são IllegalArgumentException e NullPointerException . A primeira acontece quando se passa um parâmetro para um método e o método não o pode usar. A segunda acontece sempre que tentar invocar um método numa variável de objeto não inicializada ( null ). Isso é bastante comum e por isso ela é, provavelmente, a exceção mais reportada de todas.

Exceções deste tipo existem em outras linguagens orientadas a objetos que implementam o conceito de exceção e são as que nos devem preocupar mais enquanto programamos, já que, traduzem situações que desafiam a lógica do programa.

Exceções de Contingência

Exceções de Contingência são aquelas que a aplicação pode causar ou não, mas que tem com as quais tem que lidar explicita e imediatamente. Exceções de Contingência são aquelas que descendem diretamente de Exception excepto as que descendem de RuntimeException.

Devido ao nome sugestivo é comum confundir o conceito de exceção com a própria classe Exception . Tenha sempre em mente que ao falarmos de exceções e tratamentos de exceções referimo-nos à hierarquia derivada de Throwable, a qualquer um dos conceitos acima, ao mecanismo em si e não apenas às classes nem a uma classes em particular.

As exceções de contingência têm este nome porque frequentemente representam exceções para as quais o programa deve ter um plano de contingência. O exemplo clássico é a exceção FileNotFoundException que significa que o arquivo que estamos interessados, não existe. Isto é uma exceção no sentido que o programa espera que o arquivo exista, contudo, se ele não existir o programa deve ter um plano alternativo, que pode ir de simplesmente não fazer nada até apresentar uma mensagem ao usuário final ou invocar outro método que irá buscar o arquivo em outro lugar (na rede ou via FTP, por exemplo), ou simplemente criá-lo primeiro.

Ambientes que não se controlam não são confiáveis. Não se pode assumir que esses ambientes funcionam corretamente, ou sequer que funcionam. Como acessar outros ambientes é o que uma aplicação normalmente faz é necessário que as exceções que daí decorrem sejam exceções de contingência.

Em Java, todas as exceções de contingência são exceções verificadas.

Exceções verificadas e não-verificadas

Java foi a primeira linguagem a introduzir o conceito de exceção verificada. Por padrão as exceções em Java são verificadas.

A aplicação é obrigada a lidar com estas exceções explicita e imdiatamente na medida que o método que recebe este tipo de exceção é forçado a verificar se pode resolver o problema. Se o método sabe que não poderá resolver a exceção, ele deve declarar isso explicitamente.

Note que este mecanismo de verificação é muito útil quando temos que antever planos de contingência para a ocorrência da exceção. O uso da palavra contingência não é coincidência. Java entende que para todas as exceções devem existir planos de contingência; alternativas que permitam que o programa continue normalmente. Assim todas as classes que descendem diretamente de Throwable ou Exception são exceções verificadas.

Mas o mundo não é perfeito e Java entende também que existem exceções para as quais não é possível antever um plano de contingência, Erros e Falhas, são esses tipos. Os primeiros nunca deveriam acontecer, portanto, nem faz sentido ter planos para os resolver. Os segundos podem até demonstrar que a aplicação está funcionado corretamente ao identificar aquele problema. Por isso Error e RuntimeException são exceções não verificadas.

verifcadasvsnao

O fato de RuntimeException herdar de Exception confunde muita gente porque parece significar que todas as RuntimeException são também Exception. Isso significaria que todas as falhas poderiam ser resolvidas se existirem planos de contingência para elas. O que é verdade, já que todas as falhas são possíveis exceções de contingência. Na verdade, é por isso que todas as RuntimeException são também Exception . Elas podem ser resolvidas se o programa tiver como e desse ponto de vista a sua resolução é igual à de Exception.

Mas RuntimeException herdar de Exception parece significar que o comportamento de exceção verificada é também herdado, o que não é verdade. O mesmo argumento poderia ser usado com Error e Throwable . A verificação é algo que o compilador obriga. A verificação é, portanto, uma característica da linguagem, do compilador, que nada tem a ver com herança. Se este conceito é difícil de entender, pense apenas que as várias classes de exceção são marcadores para o compilador e a JVM sabe como e quando obrigar a lançar/capturar a exceção que elas representam.

throw e throws

Para lançar uma exceção, em java, simplesmente usamos a instrução throw seguida do objeto que representa a exceção que queremos lançar. O conceito é semelhante ao de return, mas enquanto return está devolvendo um resultado de dentro do método, throw está lançando uma exceção.

	public double divide (double dividendo , double divisor) throws ArithmeticException {
		 if (divisor==0){
		  // não queremos poder dividr por zero.
		  throw new ArithmeticException(“Divisor não pode ser zero");
		}

		// o resto do codigo
		return dividendo / divisor;
	}

Aqui temos um exemplo de um código que lança uma exceção se tentarmos dividir por zero. O código usa throw para lançar a exceção, e usa throws para documentar qual exceção o método pode lançar.

Documentando o lançamento

Quando a exceção é não-verificada, não é obrigatório indicar o seu lançamento na clausula throws mas sempre é necessário documentar a razão que irá lançar essa exceção. A forma mais simples de documentar é usar o Javadoc e a tag @throws. O código final seria então:

/**
	Divide dois números double. O divisor não pode ser zero.

	@param dividendo o numero a dividir
	@param divisor o numero pelo qual dividir. Não pode ser zero.
	@return o quociente da divisão.
	@throw ArithmeticException se o divisor for zero.

*/
public double divide (double dividendo , double divisor) throws ArithmeticException{
	// o resto do codigo
}

É sempre importante documentar as exceções, verificadas ou não, que o método pode lançar e as condições em que elas serão lançadas. Esta prática dá informação a quem usar o método do tipo de condições em que o método pode ser usado.

Capturando Exceções

Para capturar exceções, em Java, usamos o tradicional bloco try-catch-finally

	try {
		// aqui executamos um, ou mais, métodos
		 // que podem lançar execções.
	}catch (Throwable e) {
		// aqui a exceção aconteceu e tentamos evitar o problema
		// fazendo a operação de modo diferente
	}

Podemos declarar mais do que um bloco catch . Isso é importante porque podemos ter vários tipos diferentes de exceção sendo lançados e necessitar de um tratamento específico para cada um.

	try {
	// aqui executamos um método que tenta ler um arquivo

	}catch (FileNotFoundException  e) {
	 // se o arquivo não existir esta exceção é lançada.

	 // aqui colocamos a resolução
	}catch (EOFException e) {
	 // quando esta exceção aoontece significa que aconteceu
	 // um problema na leitura do arquivo.

	 // aqui colocamos a resolução
	} catch (IOException e) {
	 // uma outra exceção de I/O aconteceu.

	 // aqui colocamos a resolução
	}

Pode acontece que durante a tentativa de resolução do problema, cheguemos à conclusão que não podemos fazer mais nada e o problema é irresoluvel. Nesse caso, é possível usar [.keyword]`throw, ` dentro do bloco [.keyword]`catch ` para relançar a exceção que capturámos. Tudo se passa como se ela nunca tivesse sido apanhada.

A ordem pela qual devemos colocar os blocos catch não é aleatória. Se usarmos classes de exceção de uma mesma hierarquia, a classe mais genérica tem que ser capturada depois das outras da sua descendência. No exemplo anterior, FileNotFoundException e EOFException derivam de IOException ` por isso ela é capturada depois das outras duas. Isto é assim porque a JVM irá comparar a classe da exceção que aconteceu com a classe declarada em [.keyword]`catch e usar o primeiro bloco que for compatível. Se a classe mais genérica estiver antes, ela seria sempre a escolhida nunca dando chance de usar os outros blocos. Se você tentar fazer isso, o compilador irá reclamar, protegendo a lógica do mecanismo de tratamento.

Try-Finally

Por vezes, mesmo sabendo que os métodos que estamos usando lançam exceções, sabemos também que não podemos fazer nada para as resolver. Nesse caso, simplesmente não usamos o bloco try-catch e simplesmente declaramos as exceções com throws na assinatura do método. Mas, e se, mesmo acontecendo uma exceção existe um código que precisamos executar ? É neste caso que usamos o bloco finally

Este tipo de problema é mais comum do que possa parecer. Por exemplo, se você está escrevendo num arquivo e acontece um erro, o arquivo tem que ser fechado mesmo assim. Ou, se você está usando uma conexão a banco de dados e acontece algum problema a conexão tem que ser fechada.

Para usar o bloco try-finally , começamos como envolver os métodos que podem lançar exceções como vimos antes, mas usamos um bloco finally em vez de um catch.

	try {
		// aqui executamos um método que pode lançar uma exceção que não
		// sabemos resolver
	} finally {
		// aqui executamos código que tem que ser executado, mesmo que um problema aconteça.
	}

Isto é muito útil, mas pense o que acontece se dentro do bloco try colocamos um return. Isso significa que algo tem que ser retornado para fora do método, mas significa também que o método acaba aí. Nenhum código pode ser executado depois de um return (o compilador vai-se queixar dizendo que o código seguinte é inalcançável). Isso é tudo verdade, exceto se esse código suplementar estiver dentro de um bloco finally . O código dentro do bloco finally não apenas é executado se uma exceção acontecer, mas também se o método for interrompido. É garantido que o código dentro do bloco finally sempre será executado, aconteça o que acontecer. Este é um outro uso importante deste bloco.

Try-Catch-Finally

Este bloco é apenas a conjunção dos anteriores. Apenas é necessário deixar claro que o bloco finally tem que ser declarado depois de todos os blocos catch A Listagem seguinte mostra o uso de todos os conceitos e palavras chave relacionados ao mecanismo de exceções.

SQLException é uma exceção de contingência, e portanto verificada, mas nem sempre é claro como tratar esse tipo de exceção. Isso acontece porque na realidade essa exceção representa uma imensidão de exceções diferentes. A especificação JDBC 4.0 veio melhorar este cenário definindo classes derivadas mais especificas.

	// faz uma consulta SQL ao banco retornando todos os produtos
	public List<Produto> queryAllProducts () throws SQLException {

	// Para podermos usar o objeto con dentro do try e do finally
	// precisamos declará-lo fora de ambos os blocos.
	  Connection con = null;
	  try {
	    // obtém conexão. Não nos importa muito como.
	    con = this.getConnection();

	    // executa comando SQL
	    ResultSet rs = con.createStament().executeQuery("SELECT * FROM PRODUTOS");

	    // mapeia o ResultSet para uma lista de objetos
	    List<Produto> resultado = mapResultSet(rs,Produto.class);

	    // retorna o resultado.
	    // O código no bloco finally ainda será executado.
	    return resultado;
	  } catch (SQLException e) {
	    // descobre se a falha se deve à tabela não existir no banco
	    if (this.exceptionMeansTableMissing(e)){
	     // realmente a tabela não exite no banco.
	     // retorna uma lista vazia.
	     return Collection.emptyList();
	    } else {
	     // não conseguimos resolver o problema.
	     // relançamos a exceção
	     throw e;
	    }
	  } finally {
			// fecha a conexão
	    con.close();
	  }
	}

Try-With-Recources

Java tem ainda outra forma de captural exceções que simplifica o uso de finally para liberar recursos.

Esta forma é chamada try-with-resource e permite declarar um recurso que será fechado no fim do bloco tenha acontecido uma exceção ou não.

	// faz uma consulta SQL ao banco retornando todos os produtos
	public List<Produto> queryAllProducts () throws SQLException {

	  try ( Connection con = this.getConnection()){
	    // obtém conexão, dentro do bloco.

	    // executa comando SQL
	    ResultSet rs = con.createStament().executeQuery("SELECT * FROM PRODUTOS");

	    // mapeia o ResultSet para uma lista de objetos
	    List<Produto> resultado = mapResultSet(rs,Produto.class);

	    // retorna o resultado.
	    // O código no bloco finally ainda será executado.
	    return resultado;
	  } catch (SQLException e) {
	    // descobre se a falha se deve à tabela não existir no banco
	    if (this.exceptionMeansTableMissing(e)){
	     // realmente a tabela não exite no banco.
	     // retorna uma lista vazia.
	     return Collection.emptyList();
	    } else {
	     // não conseguimos resolver o problema.
	     // relançamos a exceção
	     throw e;
	    }
	  } // não é necessário o finally.
	}

Esta forma é mais segura que o uso tradicional de finally`que vimos antes. Contudo, é necessário que o objeto declarado seja um recurso. Em Java, um objeto é considerado um recurso se implementar a interface `AutoCloseable.

Resumo

O mecanismo de exceções, em Java, é baseado no lançamento e captura de objetos da classe Throwable. Este mecanismo é diferente do mecanismo de retorno de resultados invocado quando usamos return e por isso existe a palavra throw , que lança a exceção e dá início ao mecanismo de lançamento e captura de exceções.

Exceções, em Java, podem ser verificadas ou não. As exceções verificadas obrigam o código a lidar imediatablemente com a exceção. Exceções verificadas são especificas a Java e não existem em outras linguagens. A verificação é um regra do compilador e não do runtime.

Exceções verificadas são normalmente usadas em código que acesse recursos fora da memória da máquina, como o sistema de arquivos ou a rede. Java parte do principio de que ambientes que ele não controla não são confiáveis e que deve sempre haver pelo menos uma chamada de atenção do programador sempre que ha interação com esses ambientes.

Podemos capturar e tratar exceções usando uma conjunção do blocos try-catch-finally . É garantido que o código no bloco finally sempre é executado, mesmo que exista uma exceção e mesmo que o bloco try contenha a instrução return . Esta funcionalidade especial do bloco finally é importante quando temos que fazer operações de limpeza, como fechar conexões, antes de sair do método e mesmo que não existam exceções. É tão importante que podemos usar a variante try-with-resource

Bibliografia

[1] Exceções - Conceitos e Práticas (Code Design World)

[2] The Java Tutorials – Lesson Exception, Sun Microsystems, Inc. (http://java.sun.com/docs/books/tutorial/essential/exceptions/)

[3] Does Java need Checked Exceptions?, Kevlin Henney (http://www.mindview.net/Etc/Discussions/CheckedExceptions)

[4] JDBC 4.0 Enhancements in Java SE 6, Srini Penchikala (http://www.onjava.com/pub/a/onjava/2006/08/02/jjdbc-4-enhancements-in-java-se-6.html)

Scroll to Top