Considere um método que deve
receber como parâmetro uma referência para um arranjo de arranjos representando
uma matriz quadrada. O que deve ser feito se este método receber uma referência
para um arranjo representando uma matriz que não seja quadrada? A linguagem
Java tem uma boa disciplina para separar o que é convencionado como
“normal e esperado” e o que é considerado
“excepcional”. Tanto a Máquina Virtual quanto um programa podem (i)
declarar ou “lançar” erros e exceções (via comando throw), tanto a Máquina Virtual quanto um
programa podem (ii) tratar ou “apanhar” erros e exceções utilizando
o comando try-catch e um programa pode ainda (iii) declarar que não
trata erros e exceções.
As situações de erro e
exceção são materializadas na forma de uma referência para uma instância de uma
classe que representa o problema. Toda situação considerada de erro ou exceção
corresponde ao arremesso ou lançamento (throw) de uma referência de uma
instância de uma classe adequada.
A linguagem Java tem três
classes notáveis: (i)String, (ii)Object e
(iii)Throwable.
A classe String é a única classe que tem literais (representação de
constantes). A classe Object é a única que não estende nenhuma classe em sua
definição. A classe Throwable é a única classe cujas instâncias diretas ou
indiretas podem ter suas referências lançadas. A classe Throwable é
pré-definida na linguagem Java, juntamente com várias outras classes definindo
um grande número de categorias de erro. Os programadores Java, por convenção,
caso queiram criar novas exceções devem estender a classe Exception que é uma subclasse de Throwable.
Duas classes descendentes da classe Throwable podem ser consideradas especiais:
a classe Error e a
classe RuntimeException. A linguagem Java não obriga o programador a tratar os lançamentos de
exceção envolvendo instâncias da classe Error, da classe RuntimeException e das
classes delas derivadas, todos os outros lançamentos de exceção devem ser
tratados pelo programador.
A Máquina Virtual Java
lança referências para instâncias de maneira implícita utilizando as classes
pré-definidas. O programador pode utilizar o comando throw de forma
explícita. Quando o fluxo de controle atinge o comando throw
<expressão>,
a expressão é avaliada. Esta expressão corresponde, em geral à criação de um
objeto e resulta numa referência, p. ex. throw new ErroDoTipoX();. A partir daí o fluxo de controle
será desviado para uma cláusula catch apropriada de algum comando try-catch.
O fluxo de controle segue a cadeia dinâmica dos Registros de ativação das
invocações dos métodos, ou seja a execução de um método pode terminar (i) por
que o fluxo de controle atingiu o final do método (return implícito, só pode
ocorrer no caso de métodos do tipo void!), (ii) porque o fluxo de controle
atingiu um comando return, ou (iii) porque foi executado um trow implícito ou
explícito que não foi apanhado por um comando try-catch daquele método. A
procura por uma cláusula catch apropriada propaga-se na direção do método main
e, não havendo tratamento, para a MVJ, neste caso a execução do programa será
finalizada, com mensagem de erro provida pela MVJ dizendo que uma exceção foi
lançada sem que fosse apanhada.
A linguagem Java classifica
os erros e exceções em dois grupos: (i) o grupo que obriga a verificação (checked)
e (ii) o grupo que não obriga verificação (nonchecked). Até este ponto
da disciplina sempre utilizamos programas lidando apenas com erros e exceções
do grupo que não obriga a verificação.
Para lançar uma exceção com
referência para uma instância das classes de verificação obrigatória a
linguagem obriga o programador a declarar no cabeçalho do método quais as
classes de exceção podem ter instâncias lançadas. Portanto, o formato completo
do cabeçalho de definição de um método é:
<modificadores>
<tipo> <nome>(<parametros>) throws <classes>
static void m() throws ErroDoTipoX, ErroDoTipoY, ErroDoTipoZ{
...
throw new ErroDoTipoX();
...
throw new ErroDoTipoY();
...
throw new ErroDoTipoZ();
}
Observe que a cláusula
throws do cabeçalho de um método tem implicações na questão da definição da
relação extends entre classes. Se o programador quiser estender a classe que
contém o método m acima e se o programador quiser sobrepor o método m então o
novo método terá de declarar o lançamento de instâncias das mesmas classes. Isso
garante que um código que trabalha com a classe base trabalhará também com as
classes derivadas.
O programador pode lidar
com o lançamento de exceções de verificação obrigatória de duas maneiras. A
primeira opção é utilizar um comando que pode dar origem à exceção dentro do
bloco try de um comando try-catch definindo uma cláusula catch
adequada. A segunda opção é utilizar a cláusula throws do cabeçalho de
definição de um método.
O programador pode decidir não tratar naquele método uma exceção que exige verificação,
mas a linguagem obriga o programador a avisar isto através da cláusula throws.
Os programas abaixo
ilustram estes conceitos.
class tstthrow{
static int hexChar2int(char c) throws Exception{
if(c>='0'& c<='9') return c-'0';
if(c>='A'&
c<='F') return c-'A'+10;
if(c>='a'& c<='f') return c-'a'+10;
throw new
Exception("caractere nao
e hexadecimal!");
}
public static void
main(String[] args){
try{
int i=hexChar2int('G');
System.out.println("Tudo
ok! i iniciou
com:"+i);
}
catch(Exception p){
System.out.println("problema:"+p);
}
}
}
Observação:
Exception não é a melhor classe para o caso do programa acima. Uma melhor opção
seria o uso da classe IllegalArgumentException. Exercício: reescreva o programa
acima substituindo a classe Exception pela classe IllegalArgumentException.
Neste programa foi usada a classe Exception para poder ilustrar a diferença
entre exceções que precisam ser declaradas na cláusula throws da declaração de
um método. As subclasses da classe RuntimeException não precisam constar na
cláusula throws! A classe IllegalArgumentException é uma subclasse da classe
RuntimeException.
No programa
acima o método hexChar2int para poder lançar uma instância da classe Exception
e declara isso no seu cabeçalho. O método main não precisa declarar isso no seu
cabeçalho porque a invocação do método hexChar2int está dentro do bloco try de
um comando try-catch. O programa abaixo não compila:
class tstthrow{ /* este programa nao compila */
static int hexChar2int(char c) throws Exception{
if(c>='0'& c<='9') return c-'0';
if(c>='A'& c<='F') return c-'A'+10;
if(c>='a'& c<='f') return c-'a'+10;
throw new Exception("caractere nao e
hexadecimal!");
}
public static void main(String[] args){
int i=hexChar2int('G');
System.out.println("Tudo ok! i
iniciou com:"+i);
}
}
A tentativa de compilação do programa acima resulto no seguinte erro:
Exception must be caught, or it
must be declared in the throws clause of this method.
Uma outra
solução portanto, caso o programador não queira lidar com exceções utilizando o
comando try-catch, é declarar que o método main pode lançar (ou deixar passar) lançamentos
de instâncias da classe Exception:
class tstthrow{
static int hexChar2int(char c) throws Exception{
if(c>='0'& c<='9') return c-'0';
if(c>='A'& c<='F') return c-'A'+10;
if(c>='a'& c<='f') return c-'a'+10;
throw new Exception("caractere nao e
hexadecimal!");
}
public static void main(String[] args) throws Exception{
int i=hexChar2int('G');
System.out.println("Tudo ok! i
iniciou com:"+i);
}
}
Ou seja, não
existem exigências com relação às exceções do grupo nonchecked. As
demais exceções para serem lançadas devem ter suas classes listadas no
cabeçalho do método lançador. Um método que invoca um outro método lançador de
exceções checked deve se precaver através de um comando try-catch adequado ou
então deve declarar que deixa passar os lançamentos.
A figura abaixo ilustra o
relacionamento entre as exceções checked e nonchecked e a
hierarquia de classes com raiz na classe Throwable. Os retângulos com nomes representam
classes e os triângulos representam hierarquias de classes com classe
“raiz” identificada pelo retângulo da ponta superior do triângulo.
Na hierarquia de classes predefinida em Java a classe Throwable tem somente
duas classes filhas (Error e Exception), mas um programador Java pode definir
outras hierarquias a partir da Throwable (embora isso não seja recomendável).
As classes e hierarquias cujo lançamento de instâncias seja de verificação
obrigatória são identificadas na cor cinza.Todas as instâncias diretas e
indiretas da classe Error e da classe RuntimeException são de verificação
opcional. Todas as instâncias diretas e indiretas da classe Throwable, que não
sejam as instâncias diretas e indiretas das classes Error e RuntimeException,
são de verificação obrigatória.

A boa
disciplina sobre a escolha de uma categoria de exceção/erro
ser do tipo checked ou nonchecked relaciona-se à possibilidade de recuperação. Observe por exemplo que a
indexação errada de um arranjo configura
um problema de programação que não deveria ocorrer e portanto é dificil
imaginar que isso deve ser algo recuperável: não sendo recuperável não obrigue
o programador a cuidar disso, afinal se ele fez sua tarefa de forma correta
isto não vai acontecer. Uma aplicação pode verificar que um dispositivo de
armazenamento está disponível e em algum momento posterior este dispositivo
pode não estar mais disponível: é razoável supor que uma aplicação queira se
recuperar com relação a um dispositivo não disponível (por exemplo para salvar
um arquivo em outro dispositivo!), daí obrigue o programador a cuidar desta
categoria de erro!
Quando é
feito o lançamento de um erro/exceção e é encontrado um comando try/catch, as
cláusulas catch são verificadas sequencialmente. As classes das cláusulas
catch, caso tenham relacionamento, só podem aparecer da mais específica para a
mais genérica:
class prog{
public static void main(String[]
x){
try{
throw new MinhaExcecao();
}
catch(MinhaExcecao p){}
catch(Exception p){}
}
}
class MinhaExcecao extends Exception{}
O programa não compila se classes mais genéricas precedem classes mais
específicas:
class prog{
public static void main(String[]
x){
try{
throw new MinhaExcecao();
}
catch(Exception p){}
catch(MinhaExcecao p){}
}
}
class MinhaExcecao extends Exception{}
Conforme
foi dito acima ao ser executado um lancamento de erro/exceção o fluxo de controle
faz uso dos registros de ativação e, na direção do registro de ativação mais
recente para o registro de ativação mais antigo é procurado um comando
try/catch que pegue o lançamento. O programa abaixo ilustra o retorno do fluxo
de execução ao longo de execuções aninhadas de métodos. O método main() invoca
o método m(), m() invoca o método n(), n() invoca o método o(), o() invoca o
método p(), quando p() lança uma instância da classe Exception, o fluxo de
controle não encontra um try/catch adequado em p(), destroi o registro de
ativação de p() e procura um try/catch adequado no escopo do método que invocou
p() [no caso método o()]. Não encontrando um try/catch adequado em o(), destroi
o registro de ativação de o() e procura um try/catch adequado no escopo do
método que invocou o(). Isto ocorre até que um “catch” apropriado
seja encontrado, neste caso o try/catch do método main(). A única escrita é o
println() do método main().
class tstcatch{
static void m()throws Exception{
n();System.out.println("m");}
static void n()throws Exception{
o();System.out.println("n");}
static void o()throws Exception{
p();System.out.println("o");}
static void p()throws Exception{
throw new Exception();}
public static void main(String[]
x){
try{ m();}
catch(Exception p){
System.out.println("o
fluxo de controle veio desde p()!");
}
}
}
A sintaxe
básica do comando try/catch (na verdade try/catch/finally!):
try{
<comandos que podem ou nao lançar exceções>
} catch(ExcecaoTipo1 parametro){
<tratamento da instância da classe ExcecaoTipo1>
} catch(ExcecaoTipo2 parametro){
<tratamento da instância da classe ExcecaoTipo2>
} ...
} finally{
<estes comandos serão excutados com ou sem lançamento de
exceção>
}
Quando o fluxo de controle
chega ao comando try/catch/finally o fluxo de controle passa a percorrer o
bloco try. Se uma instância de classe de exceção é lançada cada cláusula catch
é examinada e o parâmetro da cláusula catch "mais adequada"
recebe a referência da instância lançada e o fluxo de controle passa a
percorrer o bloco da cláusula catch. Nenhuma outra cláusula catch é executada!
Se nenhuma cláusula catch for adequada o lançamento da exceção será verificado
por comandos try externos. O bloco da cláusula finally é opcional, mas se
estiver presente será sempre executado não importando se o bloco try terminou a
execução de forma normal (e não houve bloco catch executado) ou se o bloco try
terminou de forma excepcional. Isto pode gerar situações curiosas:
class tstfinally{
public static void main(String[] arg){
System.out.println(desafio());
}
static int desafio(){
try{ return 1;}
finally{ return 2;}
}
}
O que é impresso pelo programa acima? (edite, compile e execute!) Existe uma
"razão" do fluxo de controle entrar no bloco finally, em particular
esta razão pode ser pelo fato de ter sido executado um comando return que
termina a execução do bloco try de forma não excepcional. Ao entrar no bloco
finally a "razão" original pode ser "esquecida". Por outro lado se o fluxo de execução for
desviado para o método exit() da classe System o fluxo não retorna!
class tstcatch{
public static void main(String[]
x){
try{ System.exit(0);}
finally{
System.out.println("o
fluxo de execução não vem aqui!");
}
}
}
Um uso mais
“normal” do “finally” é garantir que algo seja feito,
ao final da execução de um trecho de comandos (que não tenha System.exit()!)
onde vários tipo de problemas podem acontecer:
aparelho.liga();
try{ //vários métodos}
catch(TipoDeErro1 p){// ...}
catch(TipoDeErro2 p){// ...}
finally{aparelho.desliga();}
Com o trecho de programa
acima podemos ter certeza de que, aconteça o que acontecer, o aparelho será
desligado.
As cláusulas finally
obedecem a sequência de verificação da “pilha”de registros de
ativação:
class tstcatch{
static void m()throws Exception{
try{n();} finally{System.out.println("m");}}
static void n()throws Exception{
try{o();} finally{System.out.println("n");}}
static void o()throws Exception{
try{p();} finally{System.out.println("o");}}
static void p()throws Exception{
throw new Exception();}
public static void main(String[]
x){
try{ m();}
catch(Exception p){
System.out.println("o
fluxo de controle veio desde p()!");
}
}
}
O assunto tratamento de
exceções pode ter aspectos muito complexos e sofisticados, em particular
depende bastante da noção que existe na linguagem Java de término
“Normal” e “Abrupto” de um comando. A construção de
programas robustos depende de lembrarmos que, em qualquer ponto, algo inusitado
pode acontecer. Considere por exemplo o comando throw new ClasseX(); um dos possíveis eventos é que não
seja possível a construção da instância e neste caso uma outra instância é
lançada (p.ex. NullPointerException, OutOfMemoryError).
Nesta disciplina iremos
muitas vezes simplificar alguns programas deixando de mostrar qual seria o
tratamento adequado para algumas exceções. No material sobre entrada e saída ao
invés de termos textos de programas do tipo:
public static void main(String[] arg){
...
try{
“comportamento relacionado
com entrada e saída”;
}catch(ProblemaES1 p){...}
catch(ProblemaES2 p){...}
...
}
usaremos textos do tipo
public static void main(String[] arg) throws IOException{
“comportamento relacionado
com entrada e saída”;
}
Abaixo vemos exemplos de
métodos (vindo do material de métodos estáticos) reescrito para lançar
exceções:
static int[]
char2intDig(char[] x){
if(x==null) return null;
int[]
y=new int[x.length];
for(int
i=0; i<x.length; i++){
if(!Character.isDigit(x[i]))
throw new IllegalArgumentException("Encontrado:"+x[i]+", esperado digito!");
y[i]=x[i]-'0';
}
return y;
.}
São exercícios
interessantes a reescrita de métodos, construtores etc com vistas às exceções:
static int somaProdPeso(int[]
digito, int[] peso){
if(digito==null || peso==null)
throw new
IllegalArgumentException("argumento nulo!");
if(digito.length!=peso.length)
throw new
IllegalArgumentException("argumentos devem ter tamanhos iguais");
int somaProd=0;
for(int i=0; i<digito.length;
i++) somaProd=somaProd+digito[i]*peso[i];
return somaProd;
}
Exercícios