Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação

Algoritmos e Estruturas de Dados I

Throw & throws

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

  1. Reescreva  a classe ArabicoParaRomano (exercícios do material Classes) para lançar uma exceção no caso de um argumento que não esteja no intervalo de 0 a 3999.
  2. Reescreva a classe Cdado (do material Classes) para que o contrutor Cdado(int i) passe a lançar uma exceção no caso do argumento ser menor que 1 ou maior que 6.
  3. Reesecreva a classe CPF (do material Classes) para que o contrutor CPF(int i) passe a lançar uma exceção no caso do argumento ser menor que 0 ou maior que 999999999.
  4. Exceções só devem ser usadas em situações excepcionais! O trecho de programa abaixo é um exemplo do uso incorreto do fato do método charAt() lançar objetos do tipo IndexOutOfBoundsException. Reescreva o trecho abaixo de forma a obter o mesmo efeito mas em conformidade com a boa disciplina.
    try{
      int i=0;
      while(true) m(s.charAt(i++));
    catch(IndexOutOfBoundsException e){}
  5.