quarta-feira, 9 de março de 2011

Testando as novas funcionalidades do JDK 7 - Parte 2

Esse post é a segunda e última parte dos posts sobre os novos recursos do Java 7. Na minha opinião, os recursos apresentados nesse post são mais interessantes do que aqueles apresentados no post anterior porque causam mudanças mais significativas na linguagem.

Caso você tenha interesse no primeiro post, ele pode ser encontrado aqui!
O código fonte dos programas utilizados como exemplo nos dois posts estão disponíveis no Githut em: https://github.com/wrpinheiro/diversos.

Boa leitura.

4. Inferência na Criação de Objetos de Tipos Parametrizados (Generics)

O suporte para tipos parametrizados (generics), incluído no Java 5,  é certamente um aliado do desenvolvedor que facilita tanto a codificação quando a depuração. Com essa abordagem é possível informar tipos como parâmetros para classes e interfaces, fornecendo informações adicionais para o  compilador detectar erros em tempo de compilação que antes eram detectados somente em tempo de execução.

Apesar das vantagens do Generics, com ele a linguagem também ganhou uma verbosidade extra! Veja um exemplo na classe InferenciaGenerics abaixo:

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class InferenciaGenerics {

  public static void main(String[] args) {
    Map<Integer, Set<Integer>> mapOfIntegers =
      new HashMap<Integer, Set<Integer>>();

    Integer aKey = 10;
    Set<Integer> aSet = new HashSet<Integer>();

    mapOfIntegers.put(aKey, aSet);
  }
}

No Java 7 é possível reduzir obter uma sintaxe mais enxuta para a crição dos objetos nas linhas 10 e 13 através da omissão dos tipos com uma notação apelidada de diamond:

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class InferenciaGenerics {

  public static void main(String[] args) {
    Map<Integer, Set<Integer>> mapOfIntegers =
      new HashMap<>();

    Integer aKey = 10;    
    Set<Integer> aSet  = new HashSet<>();

    mapOfIntegers.put(aKey, aSet);
  }
}

Com essa sintaxe os parâmetros de tipos usados na construção dos objetos são inferidos a partir dos parâmetros definidos na referência para esse objeto, ou seja, em uma declaração do tipo: Tipo<t1, t2, ..., tk> referencia = new Tipo<>(), o compilador entenderá que new Tipo<>() deverá ser substituído por new Tipo<t1, t2, ..., tk>().

Além disso, também é possível postergar a criação do objeto do tipo HashSet na linha 13, fazendo-o na linha 15 da seguinte forma:
mapOfIntegers.put(aKey, aSet = new HashSet<>());

nesse caso, os parâmetros do tipo são inferidos a partir daqueles usados na declaração da variável aSet. Note que a seguinte sentença não compila:
mapOfIntegers.put(aKey, new HashSet<>());

5. Simplificação na Invocação de Métodos Varargs

Em Java, Arrays e tipos genéricos não combinam assim tão bem!! Observe o seguinte exemplo:

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class TesteVarargs {
  public static void main(String[] args) {
    Set<Integer> aSet = new HashSet<integer>();

    List<Set<Integer>> listOfSets = Arrays.asList(aSet);
  }
}


Quando o programa acima é compilado com o Java versão <= 1.6 através do comando: javac -Xlint:unchecked Varargs.java, o compilador gera o seguinte warning:

TesteVarargs.java:10: warning: [unchecked] unchecked generic array 
creation of type java.util.Set<java.lang.integer>[] for varargs parameter List<Set<Integer>> listOfSets = Arrays.asList(aSet); ^ 1 warning

Mas repare que não há nada de errado ou estranho com esse programa. As declarações, tipos e os parâmetros estão corretos. Mas porque o compilador emitiu esse warning? Na verdade, o que acontece é que o compilador antes de gerar o bytecode faz uma conversão no programa que literalmente modifica a linha 10 que fica assim:

List<Set<Integer>> listOfSets = Arrays.asList(new Set[]{aSet});

Note que nessa conversão o Set usado para construir o array não contém mais a informação de tipo (em Java não é permitido a criação de um array usando generics) o que provoca esse warning.  Generalizando, esse warning é gerado sempre que é feita a chamada para um método com uma sintaxe do tipo

<T> Tipo<T> metodo(T... params)

sendo T e Tipo dois tipos que podem ser parametrizadas e na ocasião da chamada do método em questão as instâncias representadas pelo Array params também estão com tipos parametrizados. No exemplo apresentado T é Set<Integer> enquanto que Tipo<T> é List<Set<Integer>>.

Uma forma de evitar esse warning é incluindo a anotação @SuppressWarnings("unchecked") no método ou na linha imediatamente antes da linha no qual o compilador indicou o Warning. Apesar de resolver o problema, isso é um tanto inconveniente porque poluí o nosso código visto que a chamada desse método não pode gerar nenhum problema referente à tipagem (heap pollution seria o termo correto aqui).

Note que no exemplo apresentado não há nada que possamos fazer em nosso código para evitar esse warning a não ser adicionar a anotação de SuppressWarning. Assim, seria interessante que em casos desse tipo o compilador pudesse entender que essa chamada não pode gerar problema. Para resolver essa situação foi criado no Java 7 a anotação 

@SafeVarargs

A função dessa nova anotação é informar ao compilador que a operação de conversão forçada de Arrays com tipos parametrizados é segura, ou seja, não acontecerá o heap pollution. Quando usada na declaração de um método com a sintaxe citada, essa anotação desonera as classes que chamam esses métodos da necessidade de utilizarem o SuppressWarnings.

É importante observar que agora a responsabilidade de informar o compilador para não gerar o Warning é da classe que fornece a implementação e não da classe cliente que a usa. Dessa forma, a partir do Java 6 é possível remover todos os SuppressWarnings em casos onde esse Warning era gerado.

6. Gerenciamento Automático de Recursos & Multi-catch

Duas novidades que realmente vão facilitar a vida ao escrever programas em Java são o Gerenciamento Automático de Recursos e o Multi-catch. Utilizarei um exemplo para mostrar essas novas funcionalidades.

Suponha um programa que seja responsável por ler um arquivo texto contendo nomes de classes, um nome por linha incluindo o pacote e, para cada linha será obtida a classe através do comando Class.forName e criada uma instância dessa classe através do método newInstance da classe Class também. Suponha que os tratamentos de erros apropriados estejam feitos.

Para fazer a leitura do arquivo será utilizado um FileReader e um BufferedFileReader, mas para esse exemplo, eu criei uma classe que especializa o BufferedReader com o objetivo de simplesmente mostrar quando o método close() é chamado. Essa classe é chamada (criativamente) de MyBufferedReader e o seu código é apresentado a seguir:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;

public class MyBufferedReader extends BufferedReader {
  public MyBufferedReader(Reader r) {
    super(r);
  }

  public void close() throws IOException {
    System.out.println("fechando o BufferedStream");
    super.close();
  }
}

A seguir apresentamos a classe ResourceManagementMultiCatch que é responsável por solucionar o problema.

import java.io.FileReader;

public class ResourceManagementMultiCatch {
  private static void instantiate(String className) throws Exception {
    try {
      Class<?> clazz = Class.forName(className);
      Object o = clazz.newInstance();
      System.out.println(o.getClass().getName());

    } catch (ClassNotFoundException e) {
      System.err.println("Classe nao encontrada: " + className);
      throw e;
    } catch (InstantiationException e) {
      System.out.println("Nao foi possivel instanciar a classe: " + className);
      throw e;
    } catch (IllegalAccessException e) {
      System.out.println("Nao foi possivel instanciar a classe: " + className);
      throw e;
    }
  }

  public static void main(String[] args) throws Exception {
    MyBufferedReader br = new MyBufferedReader(new FileReader("classes.txt"));
    try {  
      String line;
      while ((line = br.readLine()) != null)
        instantiate(line);
    } finally {
      br.close();
    }
  }
}


O método main é responsável basicamente por ler o arquivo e o conteúdo de cada linha é passado para o método instantiate que tentará carregar a classe e criar uma instância.

Suponha que que o arquivos classes.txt contém somente a linha: java.util.ArrayList. Ao executarmos o programa acima é obtida a seguinte saída:

java.util.ArrayList
fechando o BufferedStream

Caso o arquivo classes.txt contivesse o nome de uma classe não existente no Java, como por exemplo: java.util.ArrayList1, obteríamos a seguinte saída:

Classe nao encontrada: java.util.ArrayList1
fechando o BufferedStream
Exception in thread "main" java.lang.ClassNotFoundException: java.util.ArrayList1
 at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
 at java.security.AccessController.doPrivileged(Native Method)
 at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:169)
 at ResourceManagement.instantiate(ResourceManagement.java:6)
 at ResourceManagement.main(ResourceManagement.java:27)

Observe no método main o uso do finally que chama o método close() do MyBufferedReader com o objetivo de liberar os recursos alocados para usar o arquivo classes.txt. Note também que, tanto executando o programa com sucesso quanto em situação de erro a mensagem: "fechando o BufferedStream" aparece, ou seja, o finally é executado e os recursos são liberados em ambos os casos.

Essa abordagem no uso do finally é muito comum quando trabalhamos com conexões e statements de BD, IO em geral, manipulação de arquivos de mídia, etc. Esses são os casos onde é necessário que o nosso programa adquira recursos do SO e esses recursos devem ser liberados posteriormente.

Para tirar esse trabalho do desenvolvedor, o Java 7 traz o gerenciamento automático de recursos. Observe o código a seguir que implementa esse recurso no método main:

public static void main(String[] args) throws Exception {
    try(MyBufferedReader br = new MyBufferedReader(new FileReader("classes.txt"))) {
      String line;
      while ((line = br.readLine()) != null)
        instantiate(line);
    }
  }
}

Com essa mudança, a declaração e inicialização do MyBufferedReader é feita no próprio try, definindo que a instância terá seu método close chamado automaticamente, de forma semelhante aquela obtida quando usamos o finally. Isso certamente evita confusões e erros na hora de liberar os recursos.

Mas como o Java sabe quais classes podem ser usadas para fazer esse tratamento? A resposta é que agora existe a interface java.lang.AutoCloseable com um único método: void close() throws Exception. Assim, qualquer classe que implementa essa interface pode ser passada para o try e o método close será chamado. Em decorrência dessa chamada implícita do close, esse try deve fazer parte de outro try contendo um catch para Exception ou o método deve lançar Exception.

Continuando com o nosso exemplo, outra novidade é o multi-catch no qual agora o catch pode tratar múltiplas exceções. Dessa forma, quando temos vários catches fazendo um mesmo tratamento para exceções diferentes, podemos juntar tudo em um único catch. No programa de exemplo, podemos aplicar esse recurso e o método instantiate fica assim:

private static void instantiate(String className) throws Exception {
  try {
    Class<?> clazz = Class.forName(className);
    Object o = clazz.newInstance();
    System.out.println(o.getClass().getName());

  } catch (ClassNotFoundException e) {
    System.err.println("Classe nao encontrada: " + className);
    throw e;
  } catch (InstantiationException | IllegalAccessException e) {
    System.out.println("Nao foi possivel instanciar a classe: " + className);
    throw e;
  }
}

Como estávamos fazendo o mesmo tratamento para as exceções InstantiationException e IllegalAccessException, então juntamos essas duas exceções num catch só. Repare que as exceções são separadas por um caractere pipe '|'. A forma como é feito o tratamento usando essa sintaxe é a mesma que a anterior.

Conclusão

Nesse post foram apresentados mais 3 recursos da linguagem Java 7, que são: Inferência na Criação de Objetos de Tipos Parametrizados (Generics), Simplificação na Invocação de Métodos Varargs e o Gerenciamento Automático de Recursos & Multi-catch. Essas alterações são maiores em relações àquelas apresentadas no primeiro post e realmente fazem diferença na linguagem.

Em especial, gerenciamente automático de recursos deve ajudar muito a resolver problemas referentes ao gerenciamento de recursos e a inferência de tipos e multi-catch ajudam a diminuir a verbosidade do Java.

Isso aí pessoal! Até a próxima.

Links Úteis