[Pesquisar este blog]

quinta-feira, 15 de fevereiro de 2018

POO::Fundamentos-12-Exceções e seu lançamento

POO-F-11-Autorreferência this POO-F-13-Tratamento de exceções
Na plataforma de programação Java, assim como em outras linguagens OO modernas, a política de tratamento de problemas ocorridos durante a execução de um programa utiliza a estratégia de criação, lançamento e tratamento de exceções e erros.

Então, é essencial realizar a distinção entre:

  • Exceções, que são problemas de severidade variável, tratáveis pelo programador; e
  • Erros, problemas de severidade alta que não tratáveis pelo programador.

Exceções e Erros

Uma exceção (exception) é um objeto especial, de uma classe específica pertencente a hierarquia da API Java, a classe Exception do pacote java.lang, que é destinada a sinalização de situações que podem ser tratadas pelo programador [4][7].

Isto significa que uma exceção indica a ocorrência de uma anormalidade no código, a qual poderia ser solucionada pelo próprio programa, por meio do uso de um dado alternativo, de uma outra estratégia de cálculo, pela obtenção de um novo dado junto ao usuário do programa, ou eventualmente outra ação considerada apropriada.

Um erro (error) é também um objeto especial, de outra classe específica pertencente a hierarquia Java que é java.lang.Error, mas reservada para indicar situações severas ou complexas, que não deveriam ser tratadas pelo programador [4][7].

Assim, um erro ocorrido durante a execução do código muito provavelmente não pode ser solucionado pelo próprio programa, pois pode ter origem num defeito do hardware, num problema junto ao sistema operacional ou ainda na própria máquina virtual Java (JVM).

Enquanto uma exceção pode estar relacionada a um problema previsível, que pode ser resolvido com algum cuidado extra no projeto do sistema; um erro é uma situação rara, imprevista, severa e crítica, cuja prudência recomenda apenas informar o ocorrido e parar imediatamente o programa.

As exceções (exceptions) geralmente estão relacionadas a situações que, embora anormais, 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.

Já os erros (errors) estão sempre associados a situações anormais, atípicas e não previsíveis pelo programador, tais como erros internos da JVM, falhas oriundas do mal funcionamento do hardware (processador e memória), defeitos (bugs) no sistema operacional, assim inconsistências decorrentes de ataques ao sistema.

Por conta disso, o termo tratamento de exceções é visto com frequência, sendo uma necessidade comum da programação. Já o tratamento de erros é raro e, por esta razão, não será tratado neste material.

A classe Throwable

Na hierarquia de classes da API Java existe a classe Throwable, do pacote java.lang, que é a superclasse de todos os erros e exceções da linguagem Java, como mostra a figura que segue. Apenas objetos deste tipo ou de suas subclasses são lançados pela JVM e podem ser lançados pelo uso da diretiva throw.

De maneira análoga, apenas objetos deste tipo, ou de suas subclasses, podem ser apanhados por cláusulas catch, que fazem parte da diretiva try.

A diretiva try e a cláusula catch, ou simplesmente try/catch, compõem uma das formas mais comuns do tratamento de exceções. Já a diretiva throw serve para que um programa sinalize a ocorrência de uma situação anormal por meio de do lançamento de uma exceção [7].

Funcionamento das exceções

As exceções são criadas nos locais onde as situações problemáticas são encontradas no código do programa. Assim, como qualquer objeto, as exceções devem ser criadas e depois lançadas para o contexto superior (trecho de código que invocou a execução do trecho específico onde o problema foi encontrado) [7]. A figura que segue ilustra o funcionamento das exceções.

Isto significa que as exceções podem ser monitoradas e tratadas em locais diferentes do código (em diferentes contextos superiores), ou seja, a ocorrência anormal pode ser tratada em outro local específico do código do programa, melhorando sua organização.

A ideia central é que quando um erro ou problema ocorre, é criada e lançada uma exceção (to throw an exception). A exceção é sempre lançada para um contexto superior, isto é, para quem acionou o trecho de código problemático, separando o problema de seu tratamento. Para tratar uma exceção é necessário apanhá-la (to catch an exception). Exceções apanhadas podem ser relançadas para outro contexto superior. Caso o contexto mais alto alcance o nível da JVM, nível onde o programa foi acionado, a execução é interrompida imediatamente e sinalizada para o usuário [7].

A grande vantagem do uso do lançamento e tratamento de exceções é a possibilidade de colocar o tratamento do problema em contextos (ou escopos) diferentes. Assim, seu emprego evita o uso de códigos de erro genéricos; ou de variáveis globais para sinalização, identificação e tratamento de erros. A experiência já mostrou que o tratamento de exceções é uma estratégia muito flexível para lidar com os problemas do código de qualquer programa [4][7].

Classes de exceção

Existem muitas classes de exceção no Java, mas a classe básica é java.lang.Exception, que indica um problema de natureza geral. Todas as demais classes de exceção são derivadas de Exception, por isso tem sempre o sufixo Exception em seus nomes, como nos exemplos que seguem, onde os nomes dos tipos das exceções também indicam seu propósito específico:

  • ArrayIndexOutOfBoundsException, sinaliza o uso de índices inválidos em arrays;
  • IOException, indica problemas em operações de entrada e saída;
  • NullPointerException, ocorre quando uma referência nula (null) é usada ao invés de um objeto válido;
  • NumberFormatException, aponta problemas de representação (formato) de valores numéricos;
  • RuntimeException, serve para indicar erros gerais durante a execução de um trecho do código do programa.
A exceção java.lang.RuntimeException é bastante importante, pois toda a família derivada desta exceção tem tratamento opcional, ou seja, são exceções consideradas não monitoradas (unchecked exceptions).

Lançamento de exceções com cláusula throw

Para lançar uma exceção basta instanciar um objeto de exceção to tipo desejado e lançá-lo, por exemplo:
throw new Exception();

Usualmente as classes de exceção aceitam mensagens como argumento de seus construtores, como:
throw new NumberFormatException(“Valor inválido”);

Tais mensagens podem incluir detalhes do problema ocorrido, que podem ser usados em na parte do programa destinada ao tratamento da exceção.

Assim, na ocorrência de um problema na execução do código, sugere-se fortemente o lançamento de uma exceção de tipo adequado, que pode ser escolhida entre as muitas existentes na API Java.

Tipos de exceções

Existem dois tipos de exceções: as exceções não monitoradas (unchecked exceptions) e as exceções monitoradas (checked exceptions).

As exceções não monitoradas (unchecked exceptions) são aquelas em que o tratamento com try/catch não é obrigatório. Estas exceções são implicitamente lançadas por diversos métodos presentes na API Java (o compilador não faz menção a sua ocorrência), assim como por métodos que podem ser criados pelos programadores. De alguma forma, sinalizam problemas eventuais, considerados de menor severidade.

Já as exceções monitoradas (checked exceptions) são aquelas em que o tratamento com try/catch é obrigatório, pois sua severidade é maior, requerendo tratamento. Estas exceções são explicitamente lançadas por muitos métodos presentes na API Java, de modo que o compilador indica como erro a ausência de tratamento próprio. Como antes, o programador pode criar métodos que lancem este tipo de exceção.

Independentemente de serem monitoradas ou não monitoradas, todas as exceções do Java, quando ocorrem, são lançadas para o contexto superior e, se alcançam a JVM, provocam a interrupção do programa. Apenas o tratamento é considerado opcional no caso de exceções não monitoradas (unchecked exceptions).

Lançamento de exceções não monitoradas

O código dos programas Java tipicamente se encontra distribuído nos métodos que compõem as classes dos objetos usados. Assim, quando o código de um método produz ou (explicitamente) lança exceções não monitoradas, não é obrigatório o tratamento local nem a indicação explícita de seu lançamento, como no método converteESoma(String, String), que segue, que recebe dois argumentos do tipo String, convertendo-se em números reais e retornando sua soma.
public double converteESoma(String s1, String s2) {
   double d1 = Double.parseDouble(s1);
   double d2 = Double.parseDouble(s2);
   return d1 + d2;
}

Dois tipos de exceções podem ser lançadas por este método:
  • NullPointerException, quando o argumento s1, ou o argumento s2 é null, isto é, não indica um objeto válido;
  • NumberFormatException, quando o argumento s1, ou o argumento s2 é um objeto válido do tipo String, mas que não tem o formato adequado de um número em ponto flutuante ou inteiro.
A primeira exceção ocorre se fizermos:
converteESoma(null, "13");
converteESoma("-7.56", null);
converteESoma(null, null);

Já a segunda exceção ocorre quando algum dos argumentos não é uma String com um número válido:
converteESoma("", "64");
converteESoma("19.85", "dois");
converteESoma("0,23534", "3/4");

Observe que no código deste método, o lançamento destes dois tipos de exceção é implícito, ou seja, sem o uso da diretiva throw. Além disso, o código do método não trata a ocorrência destas exceções, nem faz qualquer indicação de seu lançamento.

Isto é possível porque as exceções lançadas são do tipo não monitorado (unchecked exceptions), que não exigem o tratamento da exceção ou indicação explícita de seu lançamento.

Nestas situações o programador deve avaliar e antever as possibilidades de ocorrências de exceções em métodos como este.

Uso da cláusula throws

A cláusula throws pode ser aplicada em um método para indicar explicitamente as exceções possivelmente lançadas em seu código, desobrigando seu tratamento local, quaisquer sejam seus tipos (checked ou unchecked). O mesmo método converteESoma(String, String) poderia ser modificado como:
public double converteESoma(String s1, String s2)
      throws NumberFormatException, NullPointerException {
   double d1 = Double.parseDouble(s1);
   double d2 = Double.parseDouble(s2);
   return d1 + d2;
}

A cláusula throws, que segue a lista de argumentos do método, lista as exceções, monitoradas ou não monitoradas, que podem ser lançadas pelo método. Isto auxilia o programador a prever os problemas decorrentes do uso do método, além de desonerar o tratamento das exceções listadas no código do próprio método.

No caso de exceções não monitoradas (unchecked exceptions), sua indicação com a cláusula throws não é obrigatória, mas torna explícito seu lançamento possível, auxiliando o programador.

No caso de exceções monitoradas (checked exceptions), sua indicação com a cláusula throws é obrigatória quando não são tratadas no código do método.

Lançamento de exceções monitoradas

No código dos métodos das classes de um programa também podem ocorrer exceções monitoradas (checked exceptions), isto é, situações anormais que devem ser tratadas pelo programador. Exemplos deste tipo de exceção são as operações de entrada e saída envolvendo o sistema de arquivos, ou de escrita e leitura em dispositivos de rede.

Se o código de um método produz exceções monitoradas, ou tais exceções são tratadas localmente, ou são explicitamente indicadas com o uso da cláusula throws.

No exemplo que segue, o método contaLinhasArquivo(String) recebe um argumento do tipo String contendo o nome do arquivo, abrindo-o e percorrendo-o para a contagem de suas linhas, fechando-o e retornando o resultado.
public int contaLinhasArquivo(String arquivo)
      throws IOException {
   int linhas = 0;
   BufferedReader br = new BufferedReader(
      new FileReader(arquivo));
   :
   return linhas;
}

Embora apenas um fragmento deste método seja exibido para simplificação do exemplo, podemos observar que a operação de abertura do arquivo pode lançar uma exceção do tipo IOException. Tal exceção é indicada na lista da cláusula throws do método, explicitando sua ocorrência e adiando seu tratamento para um contexto superior.

Se o argumento arquivo, do tipo String, for null, poderá ser lançada uma exceção NullPointerException, mas que é do tipo não monitorada, não requerendo indicação explícita de lançamento, tão pouco tratamento.

Se for preferível o tratamento local, surge uma diretiva try/catch, envolvendo o trecho onde uma exceção a ser tratada pode ocorrer, como esquematizado abaixo.
public int contaLinhasArquivo(String arquivo) {
   int linhas = 0;
   try {
      BufferedReader br = new BufferedReader(
            new FileReader(arquivo));
      :
   } catch (IOException ioe) {
      // tratamento de erros
   }
   return linhas;
}

Com o tratamento local, não se emprega a cláusula throws para a exceção tratada.

Na próxima lição veremos como efetuar o tratamento de exceções por meio da diretiva try e suas cláusulas catch e finally.


[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.

segunda-feira, 12 de fevereiro de 2018

POO::Fundamentos-11-Autorreferência this

POO-F-10-Pacotes e Namespaces POO-F-12-Tratamento de Exceções
Todo objeto, em Java, C# ou C++, dispõe de uma referência para si próprio que é representada por meio da palavra reservada this, conhecida também como referência this ou autorreferência.

A autorreferência this tem duas aplicações bastante típicas: a primeira é possibilitar a diferenciação de campos da classe de variáveis locais ou parâmetros formais; a segunda é permitir que um método retorne uma referência para o próprio objeto[1][4][5][6][7][9].


Diferenciação de elementos

Quando um método de uma classe utiliza métodos ou campos não estáticos da própria classe, implicitamente está sendo usada a referência this [4][7]. Observe os métodos e os construtores da classe Horario que segue.

public class Horario {
// atributos int de acesso restrito (privado)
   private int hor, min, seg;

// métodos de acesso (leitura) dos campos restritos
   public int getHoras () { return hor; }
   public int getMinutos () { return min; }
   public int getSegundos () { return seg; }

// métodos de alteração (escrita) dos campos restritos
   public void setHoras (int hor) {
      if(hor<0 || hor>23) {
         throw new RuntimeException("Hora invalida: " + hor);
      }
      this.hor = hor;
   }
   public void setMinutos (int min) {
      if(min<0 || min>59) {
         throw new RuntimeException("Minutos invalido: " + min);
      }
      this.min = min;
   }
   public void setSegundos (int seg) {
      if(seg<0 || seg>59) {
         throw new RuntimeException("Segundos invalido: " + seg);
      }
      this.seg = seg;
   }
   public void setHorario (int hor, int min, int seg) {
      setHoras(hor); setMinutos(min); setSegundos(seg);
   }

// construtor parametrizado
   public Horario (int hor, int min, int seg) {
      setHorario(hor, min, seg);
   }
// construtor default
   public Horario () {
      this(0, 0, 0);
   }

// representação textual dos objetos Hora
   public String toString () {
      return String.format("%02d:%02d:%02d",
         hor, min, seg);
   }
}

Observe os métodos setHoras(int), setMinutos(int) e setSegundos(int): nos três o parâmetro formal declarado tem o mesmo nome do respectivo campo da classe. O uso explícito de this permite diferenciar variáveis locais dos campos (variáveis-membro) da classe com mesma denominação.

Quando um método declara uma variável local (ou um parâmetro) com o mesmo nome de um campo da classe, a variável local oculta o campo, ou seja, quando tal nome é usado, é acessada a variável local e não o campo da classe. Analise o fragmento de setHoras(int):
if(hor<0 || hor>23) {
   throw new RuntimeException("Hora invalida: " + hor);
}

A variável hor assim indicada faz referência ao parâmetro formal do método e não ao campo da classe. Ou seja, este código testa o valor recebido pelo método. Se o valor é adequado, é atribuído ao campo da classe, distinguido da variável local por this:
// campo da classe recebe valor da variável local
this.hor = hor;

Quando não existe conflito entre os nomes presentes no escopo de um método, os campos da classe são usados automaticamente [4][7]. Na verdade, nesta situação ocorre o uso implícito da autorreferencia this, como no método toString() da classe Horario.

A figura que segue ilustra o uso de this numa outra classe.

A referência this também permite distinguir construtores sobrecarregados da classe [7], como feito na classe Horario.

O construtor Horario(int, int, int) aciona o método público setHorário(int, int, int), que por sua vez aciona os métodos setHoras(int), setMinutos(int) e setSegundos(int), os quais validam os componentes do horário.

Já o construtor Horario() aciona o construtor Horario(int, int, int) por meio do uso de this, acompanhado dos argumentos desejados:
this(0, 0, 0);

Deve ser destacado que esse uso de this só é possível em construtores e quando feito na primeira linha de código do construtor. Ao mesmo tempo, é evitada a repetição de código em construtores, prática benéfica para facilitar a manutenção da classe.

Retorno da autorreferência

Outra aplicação da autorreferência this é como retorno de métodos da classe [7].

É comum que métodos retornem novos objetos, como por exemplo, o método substring (int ini, int fim) da classe java.lang.String, que retorna um novo objeto do tipo String contendo o fragmento delimitado pelas posições ini (incluída) e fim (não incluída) do texto da String. O método toUpperCase() retorna um novo objeto String apenas com maiúsculas. Por exemplo:
// Posição             6    11
//                     v    v
String texto1 = "Peter Jandl Junior";
// texto2 recebe "Jandl"
String texto2 = texto1.substring(6, 11);
// texto3 recebe JANDL
String texto3 = texto2.toUpperCase();

Quando um método retorna um objeto, é possível, no Java e C#, encadear a chamada de métodos ou concatenar suas chamadas, assim pode ser feito:
String texto1 = "Peter Jandl Junior";
String texto3 = texto1.substring(6, 11).toUpperCase();

Aqui é acionado o método substring(int, int) do objeto texto1 (do tipo String), e o método toUpperCase(), no resultado produzido por substring, que também é do tipo String. Cada chamada de método da classe String produz um resultado String (ou de outro tipo), de modo que o objeto original não é alterado, pois, por definição de projeto, os objetos da class String são imutáveis.

Mas existem situações onde os objetos são mutáveis, isto é, podem ser alterados, como os objetos do tipo Horário, dado anteriormente neste post. Os métodos setHoras(int), setMinutos(int) e setSegundos(int) têm retorno do tipo void, portanto não permitem qualquer espécie de encadeamento.

Considere agora a classe Somador abaixo.

public class Somador {
// campo privado com total do somador
   private double total;

// construtores parametrizado e default
   public Somador(double valorInicial) {
      setTotal(valorInicial);
   }
   public Somador() {
      this(0);
   }

// método de acesso
   public double getTotal() { return total; }

// métodos de alteração
   public Somador setTotal(double valor) {
      total = valor;
      return this;
   }
   public Somador add(double valor) {
      total = total + valor;
      return this;
   }

// método de produção   
   public Somador addToNew(double valor) {
      Somador novo = new Somador(total);
      novo.add(valor);
      return novo;
   }
}

A classe Somador tem como propósito representar um somador (ou um totalizador) de valores reais. Os construtores sobrecarregados permitem tanto criar um somador com um valor inicial predeterminado, como um somador default com valor inicial zero, sem repetição de código.
// somador com valor inicial = 15.4
Somador s1 = new Somador(15.4);
// somador com valor inicial = zero
Somador s2 = new Somador();

Como o campo total é privado e, portanto, inacessível, o método de acesso getTotal() permite consultar o valor total presente do objeto.
System.out.println("Somador s1 = " + s1.getTotal());
double aux = s2.getTotal();

Existem também dois métodos de alteração. O método add(double) permite adicionar o valor do argumento ao total do objeto, mas ao invés de retornar void (típico de métodos setter), retorna a referência do próprio objeto com this (por isso o tipo de retorno de add(double) é a própria classe Somador). Isto permite o encadeamento de métodos:
// com encademento
Somador s3 = new Somador();   // total = 0
s3.add(1.5).add(3.6).add(-1); // total = 4.1

// sem encademento
Somador s4 = new Somador(); // total = 0
s4.add(1.5); // total = 1.5
s4.add(3.6); // total = 5.1
s4.add(-1);  // total = 4.1

É fácil notar que o uso do encadeamento permite obter construções mais simples. Também deve ser notado que o retorno de this permite que um mesmo objeto seja afetado (alterado) por uma série de operações encadeadas.

Finalmente, a classe Somador possui o método addToNew(double) que é considerado de produção porque retorna um novo objeto, sem alterar o objeto inicial. No código deste método, observe a instanciação de um novo somador com o valor total do objeto existente, depois a adição do valor dado como argumento e o retorno da referência deste novo objeto.
Somador s5 = s4.addToNew(10.9);
// s5 contém 15.0
// s4 continua com 4.1

Esta estratégia permite criar operações que tanto retornam novos objetos, como mantém inalterados os objetos iniciais, além de possibilitar o encadeamento de operações.

Considerações finais

O uso da autorreferência this é bastante conveniente, pois além de permitir a diferenciação de campos da classe de variáveis locais, possibilita que a sobrecarga de construtores, evitando a repetição de código.

Outro aspecto do uso de this é permitir o retorno da referência do próprio objeto, possibilitando o encadeamento de operações sobre o mesmo objeto.

POO-F-10-Pacotes e Namespaces POO-F-12-Tratamento de Exceções

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.