[Pesquisar este blog]

segunda-feira, 19 de fevereiro de 2018

POO::Fundamentos-13-Tratamento de exceções

POO-F-12-Exceções e seu tratamento POO-P-14-Objetos que usam objetos
As linguagens OO modernas utilizam as exceções para realizar o tratamento de qualquer situação anormal ou erro no código dos programas. As exceções são objetos especiais que sinalizam a ocorrência de problemas que devem ser tratados pelo programador, possibilitando a criação de código mais robusto e também seguro.

Como vimos, no Java, as exceções são objetos da família da classe java.lang.Exception, que tanto sinalizam a ocorrência de uma anormalidade, como também carregam informações adicionais sobre o problema encontrado. Embora a ocorrência das exceções, como o próprio nome indica, não seja algo normal ou desejado, são previsíveis no projeto como o recebimento de argumentos inválidos, entradas de dados inconsistentes, recursos (arquivos ou urls) não encontrados ou indisponíveis, assim como o time-out das operações executadas.

O tratamento de exceções é, portanto, uma necessidade da programação OO.

Fluxo do tratamento de exceções

O uso das exceções envolve um ciclo de tratamento simples: identificação do problema, criação, lançamento e tratamento.

Identificação do problema

Vários tipos de problemas podem ocorrer durante a execução de um problema, como um dado que não é validado ou um recurso não localizado ou indisponível. Se o problema pode ser resolvido localmente, não precisamos lançar mão do mecanismo de exceções.

Criação de exceção

Mas quando o problema detectado não pode ser resolvido no mesmo local (no mesmo método ou contexto onde foi identificado), deve ser criada uma exceção, do tipo adequado, eventualmente com informações adicionais do problema. A criação de uma exceção é uma operação de instanciação, que envolve o operador new e a classe de exceção selecionada [4][7]. As exceções criadas podem ser do tipo monitorado (checked exception) ou não monitorado (unchecked exception).


Lançamento de exceção

De posse de um objeto de exceção de qualquer tipo, é possível lançar tal exceção (to throw an exception), com uso da diretiva throw. Isto provoca a entrega do objeto de exceção lançado por throw no trecho de código do contexto superior (método que invocou a execução do método onde o problema foi encontrado) [4][7].

Tratamento de exceção

Nos locais do código onde podem ser lançadas exceções não monitoradas (unchecked exceptions), o programador pode optar pelo seu tratamento local (no contexto de sua ocorrência), pela indicação explícita de seu lançamento (para o contexto superior), ou mesmo não fazer nada (que é prática ruim).

Aqui vale lembrar que caso uma exceção, de qualquer tipo, alcance o nível da JVM, que corresponde ao contexto mais alto, pois ela acionou o programa, a execução é interrompida imediatamente e sinalizada para o usuário [7].

Quando são lançadas exceções monitoradas (checked exceptions), o programador deve optar ou pelo seu tratamento local, ou pela indicação explícita de seu lançamento para o contexto superior. Assim as exceções monitoradas exigem melhores práticas de programação!

Se a opção for o lançamento explícito, o método onde a exceção ocorre deverá empregar uma cláusula throw para indicar o lançamento da exceção, como visto no post anterior Exceções e seu lançamento.

Se a opção for o tratamento local, será necessário apanhar a exceção (to catch an exception) com uso de diretivas try associadas com cláusulas catch ou finally, como segue.


Tratamento de Exceções

Para tratarmos as exceções usamos a diretiva try, que em sua forma mais simples, emprega cláusulas catch, com a sintaxe abaixo:
try {
   // bloco de código monitorado
:
} catch (Exception e) {
   // bloco de código de tratamento
:
}

Cada par de chaves { e } delimita um bloco de código.

Aquele que segue a diretiva try é considerado o bloco de código monitorado, no qual se localizam as instruções do programa (acionamento de métodos ou outras operações) onde pode ocorrer uma ou mais exceções.

O outro bloco, que segue a cláusula catch, é o bloco de tratamento de exceções, onde as exceções de um tipo específico são apanhadas e tratadas com o código ali presente.

As exceções lançadas no bloco de código não monitorado que não apanhadas pelo bloco de tratamento de exceções seguem o fluxo usual para o contexto superior.

Se não ocorre qualquer exceção no bloco de código monitorado (bloco try), o bloco catch não é executado, ou seja, a execução sem exceções do programa desvia do bloco catch.

Mas quando ocorre uma exceção no bloco try, a execução é imediatamente desviada para o bloco catch que trata o tipo específico da exceção.

Em ambos os casos a execução continua após a diretiva try/catch, exceto quando são executadas operações de retorno, ou ocorre o encerramento do programa, ou ainda novas exceções são lançadas.

Observe o programa Java que segue, onde o método main converte o primeiro argumento recebido, de String para inteiro, exibindo uma contagem simples.
public class Contagem {
   public static void main (String args[]) {
      int max = Integer.parseInt(args[0]);
      for (int i=0; i<max; i++) {
         System.out.print(i + " ");
      }
      System.out.println();
   }
}

Neste programa, se o usuário não fornece um argumento ou fornece um argumento inválido (que não corresponde a um valor inteiro), o programa é interrompido, com a exibição de várias mensagens que podem confundir o usuário.

Por exemplo, execução do programa Contagem sem argumentos:
>java Contagem
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
        at Contagem.main(Contagem.java:3)

Ou, outro exemplo, execução do programa Contagem com um argumento inválido:
>java Contagem Jandl
Exception in thread "main" java.lang.NumberFormatException: For input string: "Jandl"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
        at java.base/java.lang.Integer.parseInt(Integer.java:652)
        at java.base/java.lang.Integer.parseInt(Integer.java:770)
        at Contagem.main(Contagem.java:3)

Neste exemplo, se o usuário não fornece um argumento válido na linha de comando, podem ocorrer dois tipos de exceção na linha:
int max = Integer.parseInt(args[0]);

Se não existe argumento (nada é digitado além do nome da classe), ocorre a exceção: ArrayIndexOutOfBoundsException, que mostra que tentamos usar uma posição do array cujo índice não é válido (no caso o índice 0 que corresponde ao primeiro argumento.

Se o argumento existe mas é inválido (não corresponde a um valor inteiro), ocorre a exceção: NumberFormatException, que indica que a conversão realizada não pode ser concluída devido ao argumento incorreto fornecido (no caso o valor que o usuário digitou).

Para resolver o problema, a linha onde ocorre a exceção deve ser substituída por um trecho que inclua o tratamento de erro. Ou seja, deveríamos substituir:
int max = Integer.parseInt(args[0]);

Pelo trecho dado a seguir:
int max = 0;
try {
   max = Integer.parseInt(args[0]);
} catch (Exception e) {
   // tratamento da exceção
   :
}

Com isso, uma opção melhor para obtermos um programa mais robusto e adequado para o usuário seria como segue:
public class Contagem {
   public static void main (String args[]) {
      int max = 0; // use zero ou outro valor default
   // código que ESTÁ sujeito a exceções
      try {
         max = Integer.parseInt(args[0]);
      } catch (Exception e) {
   // tratamento da exceção
         System.out.println("É necessário um argumento inteiro válido!");
         System.exit(-1); // finalização do programa devido ao erro
      }
   // código que NÃO ESTÁ está sujeito a exceções
      for (int i=0; i<max; i++) {
        System.out.print(i + " ");
      }
      System.out.println();
   }
}

Com este programa, caso não seja fornecido um argumento ou caso seja inválido, será exibida uma mensagem melhor para o usuário, como nos exemplos que seguem:
>java Contagem
É necessário um argumento inteiro válido!

>java Contagem Jandl
É necessário um argumento inteiro válido!

>java Contagem 7
0 1 2 3 4 5 6

Nesta situação, foi escolhido o tratamento de Exception, o tipo mais genérico de exceção, que inclui todas as outras, ou seja, inclui as exceções específicas deste programa que são ArrayIndexOutOfBoundsException e NumberFormatException.

Note que o uso de exceções evita poluir o código que se deseja executar com testes e validações diversas, feitas com diretivas if e switch, além de funções de validação. O bloco de código monitorado contém a sequência de instruções necessárias ao programa, construída para quando tudo está correto. Se ocorrem problemas, a execução é desviada para o bloco de tratamento de erros, que apenas lida com o problema encontrado.

Tratamento seletivo de exceções

Outro aspecto muito positivo da diretiva try é que são permitidas várias cláusulas catch, para exceções diferentes. Assim é possível atrelar dois ou mais blocos de tratamento diferentes para o tratamento de cada exceção possível.

A única regra para isso é primeiro apanhar as exceções mais específicas e depois as mais genéricas, assim tratar a exceção Exception será sempre a última opção, quando necessário.

O programa Contagem agora pode ter um tratamento de exceções bem sofisticado.
public class Contagem {
   public static void main (String args[]) {
      int max = 0; // use zero ou outro valor default
   // código que ESTÁ sujeito a exceções
      try {
         max = Integer.parseInt(args[0]);
   // tratamento de um tipo exceção
      } catch (ArrayIndexOutOfBoundsException e) {
System.out.println(“Faltou argumento.”);
max = 3; // aqui usamos um valor default,
                 // sem finalizar o programa
   // tratamento de outro tipo exceção
      } catch (NumberFormatException e) {
        System.out.println(“Argumento invalido.”);
System.exit(-1); // aqui finalizamos o programa
      }
   // código que NÃO ESTÁ está sujeito a exceções
      for (int i=0; i<max; i++) {
        System.out.print(i + " ");
      }
      System.out.println();
   }
}

Observe que cada exceção possível de ocorrer tem um trecho de código associado, por meio de cláusula catch, que realiza seu tratamento. Apenas um bloco catch é executado, sendo desviados os demais. Outro aspecto positivo é não não necessários testes (com diretivas if ou switch) para determinar o tipo de erro e selecionar seu tratamento.

Se o trecho de código do tratamento torna-se grande, pode ser criado um método de tratamento específico, que separa ainda mais a ocorrência do erro de seu tratamento.

Objetos de exceção apanhados

Deve ser observado que a variável e declarada nos blocos de tratamento de exceção, pode ter qualquer nome determinado pelo programador, como exc, excecao ou outro.

Esta variável permite o acesso e uso do objeto exceção apanhado, o que permite, por exemplo, obter uma String com a mensagem de erro associada ao tipo da exceção:
String msg = e.getMessage();

Tais mensagens podem conter detalhes que podem tanto ser exibidos para o usuário, quando é conveniente e adequado fornecer informações adicionais do problema; como também armazenados em arquivos de registro (logs), para análise posterior.

Também é possível imprimir a rota da exceção no console, isto é, o caminho percorrido pelo programa até a ocorrência da exceção:
e.printStackTrace();

Tal rota também pode ser obtida e processada de outras maneiras, mas isto deixa de ser básico e foge ao escopo desta lição!

Considerações finais

As exceções permitem um tratamento simples do ponto de vista de programação, mas bastante sofisticado quando consideradas as escolhas possíveis entre seu tratamento local e seu envio para contextos superiores. 

Recomenda-se que a ocorrência de erros sempre seja sinalizada com o lançamento de exceções, o que possibilita a separação clara entre o código normal (aquele executado na ausência de erros) e o código de tratamento de erros. Novamente é necessário que tal separação é uma grande vantagem, pois permite, a partir do ponto onde ocorreu exceção, desviar o fluxo de processamento para um contexto superior ou outro ponto mais adequado do programa; mas sem uso de variáveis globais, funções de erro, códigos de erro genéricos e outras técnicas improvisadas.

O tratamento seletivo de erros também possibilita melhor organização do código, o que facilita seu reuso e, principalmente, sua manutenção.


POO-F-12-Exceções e seu tratamento POO-P-14-Objetos que usam objetos

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.

Nenhum comentário: