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 outro tipo de
referência? A linguagem Java definiu uma disciplina para o programador. Tanto a
Máquina Virtual quanto o programador podem “lançar”erros e exceções
(via comando throw), tanto a Máquina
Virtual quanto o programador podem “apanhar” erros e exceções
utilizando o comando try-catch.
As situações de erro e
exceção são materializadas na forma de uma referência para uma instância de uma
classe do 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 ser usadas para serem 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, somente 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 um
try-catch apropriado é propagada até o ponto em que há um retorno para o método
main, 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. A partir de agora iremos apresentar
algumas exceções que obrigam a verificação.
Para lançar uma exceção que
seja 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();
}
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 as exceções que exigem verificação 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 que contenha 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 ou mesmo de seu/sua filho/filha
IllegalParameterException. Veja na documentação Java a diferença entre estas
descendentes da classe Throwable. 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 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".
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”. 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”;
}
Exercícios