Páginas

4 de outubro de 2011

Java 7: Modificações na Linguagem, detalhes e exemplos


VIA: [http://www.infoq.com]


O JDK 7 é certamente uma das grandes novidades no universo Java. São muitas as modificações em relação à versão anterior, que podem ser divididas nas seguintes categorias: Máquina Virtual, Linguagem, Class Loader, Internacionalização, I/O e Rede, Segurança e Criptografia, JDBC, Camada Cliente e Gerenciamento (JMX e MBeans)

As mudanças dizem respeito à JSR 334: Small Enhancements to the Java Programming Language, implementadas através do projeto Coin, um subprojeto do OpenJDK.

Separador de dígitos em literais numéricos

Com o objetivo de facilitar a leitura no código de literais numéricos longos, a partir do JDK 7 é possível utilizar o caracter "_" (sublinhado) como separador de dígitos. Dessa forma, um número como 99999999, por exemplo, pode ser escrito como: 99_999_999. Durante a compilação, o sublinhado é ignorado e o número é interpretado normalmente como se o separador não existisse. Apesar de a ideia ser interessante, é possível utilizar declarações que podem mais confundir do que ajudar, tais como:

99_99
9_9_9_9
9_____999

Isso acontece porque a única restrição para o uso do separador de dígitos é de que não seja nem o primeiro e nem o último carácter. Também é possível utilizar o separador em literais numéricos de ponto flutuante (double e float), como por exemplo:

12__3.45___6D
53_333.2397

A classe a seguir mostra como o separador pode ser usado em um programa.

  public class SeparadorLiteraisNumericos {
  public static void main(String[] args) {
 byte b =  1_2_7;
 int v1 = 999_999;
 int v2 = 100_000 - 1_000;
 Integer i1 = new Integer(12_12_12_1);
 Integer i2 = new Integer(1_2_1_2_1_2_1);
 double f = 123_456.3_4_1;

 System.out.printf("O valor de b e': %d\n", b);
 System.out.printf("O valor de v1 e': %d\n", v1);
 System.out.printf("O valor de v2 e': %d\n", v2);
 System.out.printf("i1 e i2 são iguais?: %b\n", i1.equals(i2));
 System.out.printf("O valor de f e': %.4f\n", f);
  }
}

Ao executar esse programa, a seguinte saída é obtida:

  O valor de b é: 127
  O valor de v1 é: 999999
  O valor de v2 é: 99000
  i1 é igual a i2?: true
  O valor de f é: 123456.3410

Literais Binários

Até a versão 6 do Java, era possível trabalhar com literais inteiros definidos como octais, decimais e hexadecimais. A partir da versão 7 também é possível trabalhar com literais binários. Para a grande maioria das aplicações comerciais isso pode não trazer grandes ganhos, mas em sistemas nos quais são frequentes as operações envolvendo bits, isso pode ser realmente útil. A sintaxe da nova declaração é simples, bastando iniciar o literal com 0b (zero e a letra 'b') seguido por uma sequência de 0s e 1s. Por exemplo:

0b1100
0b01
0b1101

Os dígitos são alinhados à direita na área de memória, independentemente do tipo (byte, short, etc), ou seja, 0b11 é igual a 3, 0b101 é igual a 5 e assim por diante.

O código a seguir simula um log de estados de um sensor. Os estados possíveis que o sensor pode assumir são definidos pelos seguintes padrões binários: 01 indica que o sensor está desligado; 10 indica que está ligado e 11 indica que está em alarme. Para simular esses estados foi criado o método lerEstado que devolve aleatoriamente um inteiro com esses valores possíveis de estados além de um valor 0b00 indicando que não foi possível fazer a leitura (SEM_LEITURA). No método main() é executado um laço que lê o estado actual até que seja obtido o valor inválido e imprime uma mensagem informando o estado atual. Nesse programa simples, cada estado é representado por uma constante que depois será utilizada para indexar um array de mensagens.

     public class GerenciadorSensor {
     private static final int SEM_LEITURA   = 0b00;
     private static final int DESLIGADO     = 0b01;
     private static final int LIGADO        = 0b10;
     private static final int ALARME        = 0b11;

   private static final String[] MENSAGENS = {"não disponível", "desligado", "ligado",     "alarme"};

    public static int lerEstado() {
     return (int)(Math.random()*0b100);
  }

 public static void main(String[] args) {
System.out.printf("Os estados possíveis são: %s(%d) %s(%d), %s(%d), %s(%d)\n\n", MENSAGENS[SEM_LEITURA], SEM_LEITURA, MENSAGENS[DESLIGADO], DESLIGADO, MENSAGENS[LIGADO], LIGADO, MENSAGENS[ALARME], ALARME);
int st = lerEstado();
while (st != SEM_LEITURA) {
System.out.printf("O estado do sensor é: %s\n", MENSAGENS[st]);
st = lerEstado();
 }
 }
}

Uma saída possível para esse programa é:

Os estados possíveis são: não disponível(0) desligado(1), ligado(2), alarme(3)

  O estado do sensor é: desligado
  O estado do sensor é: ligado
  O estado do sensor é: desligado
  O estado do sensor é: alarme
  O estado do sensor é: alarme
  O estado do sensor é: desligado
  O estado do sensor é: desligado

Observe que no main(), são utilizadas as constantes de estados (inicializadas com valores literais binários), para acessar as posições do array contendo as mensagens.

E combinando o novo tipo binário com a nova sintaxe de separadores de dígitos, podemos ter:

  private static final int SEM_LEITURA   = 0b0_0;
  private static final int DESLIGADO     = 0b0__1;
  private static final int LIGADO        = 0b1_0;
  private static final int ALARME        = 0b1___1;

Variáveis do tipo String em comandos switch

Uma alteração muito útil na linguagem é que agora podem ser utilizadas Strings em comandos switch. Observe o programa a seguir, que recebe parâmetros de configuração através da linha de comando, e executa acções específicas para cada parâmetro.

public class StringNoSwitch {
 public static void main(String[] args) {
 for (String param : args) {

 switch(param) {

 case "-test":
 System.out.println("O programa está sendo executado em modo teste");
 break;

 case "-silent":
 System.out.println("O programa envia poucas informações na saída padrão");
 break;

case "-verbose":
System.out.println("O programa envia muitas informações na saída padrão");
break;

default:

System.err.printf("Parâmetro %s não reconhecido\n", param);
System.exit(-1);
}
}
}
}

Note que no switch da linha 5 é utilizada uma variável do tipo String para seleccionar entre os vários casos de teste de condição. Apesar dessa ser uma alteração simples, esse recurso simplifica o código evitando uma estrutura complexa composta por varios ifs encadeados.

Inferência na Criação de Objetos de Tipos Genéricos

O suporte para tipos genéricos (ou parametrizados) é certamente um aliado do desenvolvedor, facilitando tanto a codificação quando a depuração. Com esse recurso, como sabemos, é possível informar tipos como parâmetros para classes e interfaces, fornecendo informações adicionais que permitem ao compilador detectar erros em tempo de compilação, que sem o uso dos tipos genéricos seriam detectados somente em tempo de execução.

Apesar das vantagens dos Generics, com esse recurso a linguagem também ganhou em complexidade. Veja um exemplo na classe InferenciaGenerics abaixo:

import java.util.*;
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 usar uma sintaxe para a criação dos objectos Map e Set acima, através da omissão dos tipos, com uma notação que foi apelidada de diamond:

import java.util.*;
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 objectos são inferidos a partir dos parâmetros definidos na referência para esse objecto. Ou seja, em uma declaração Tipo<t1, t2, ..., tk> ref = 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 adiar a criação do objecto do tipo HashSet (aSet), fazendo-o na última linha da seguinte forma:

mapOfIntegers.put(aKey, aSet = new HashSet<>());

Neste 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<>()); //gera erro de compilação

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

Em Java, arrays e tipos genéricos não combinam muito bem. Observe o seguinte exemplo:

  import java.util.*;
  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);
                                            
Repare, no entanto, que não há nada de errado ou estranho com esse programa. Mas porque o compilador emitiu esse warning? Na verdade, o que acontece é que antes de o compilador gerar o bytecode é feita uma conversão no programa que literalmente modifica a linha 5, que fica assim internamente:

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 é permitida a criação de um array usando generics) o que provoca o warning. Generalizando, esse warning é gerado sempre que é feita a chamada para um método com uma sintaxe do tipo a seguir:

<T> Tipo<T> metodo(T... params)
Aqui T e Tipo são dois tipos que podem ser parametrizados 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 genéricos. 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 daquela onde o compilador indicou o warning. Apesar de resolver o problema, isso é um tanto inconveniente porque polui o código sem necessidade, visto que a chamada do método não pode gerar problema em relação à tipagem (heap pollution seria o termo usado aqui).

Observe que no exemplo apresentado não há nada que possa ser feito para evitar esse warning, a não ser adicionar a anotação. Assim, seria interessante que em casos desse tipo o compilador pudesse entender que a chamada não pode gerar problema. Para resolver essa situação foi criado no Java 7 a anotação @SafeVarargs.

A função dessa anotação é informar ao compilador que a operação de conversão forçada de arrays com tipos genéricos é segura, ou seja, que 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 @SuppressWarnings. Isso é o que acontece, no JDK 7, com o método asList() da classe Arrays, como é mostrado a seguir, e também com alguns outros métodos da API.

@SafeVarargs
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

É importante observar que agora a responsabilidade de informar ao compilador para não gerar o warning é da classe que fornece a implementação e não da classe cliente que a usa.

Gerenciamento Automático de Recursos & Multicatch

Duas novidades que vão facilitar muito a vida do desenvolvedor Java são o gerenciamento automático de recursos e o "multicatch". Utilizaremos um exemplo para mostrar essas novas funcionalidades.

Suponha que precisamos de um programa que primeiro leia um arquivo texto contendo nomes de classes, com um nome por linha incluindo o pacote; e depois para cada linha, carregue e crie um objeto da classe fornecida, usando os metodos Class.forName () e Class.newInstance() da API de Reflection. Suponha também que os devidos tratamentos de erros sejam feitos.

Para fazer a leitura do arquivo seriam normalmente utilizados um FileReader e um BufferedFileReader, mas para esse exemplo foi criada uma classe que especializa BufferedReader, com o objetivo de simplesmente mostrar quando o método close() é chamado. O código dessa classe é 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();
 }
}

Aqui está a classe responsável por solucionar o problema:

import java.io.FileReader;

public class GerenciadorRecursosMultiCatch {
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.err.println("Nao foi possivel instanciar a classe: " + className);
throw e;
} catch (IllegalAccessException e) {
System.err.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 main() é responsável basicamente por ler o arquivo classes.txt. O conteúdo de cada linha é passado para o método instantiate(), que tentará carregar a classe e criar uma instância.

Suponha que o arquivo classes.txt contenha somente a linha: java.util.ArrayList. Ao executarmos o programa acima, é obtida a seguinte saída:

java.util.ArrayList
fechando o BufferedStream

Caso alguma linha do arquivo classes.txt contenha o nome de uma classe que não exista no Java, como por exemplo: java.util.ArrayList1, a seguinte saída é obtida:

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)

Observe no método main() o uso do finally na linha 28. Nele, é chamado o método close() da instância de MyBufferedReader, com o objectivo de liberar os recursos alocados para usar o arquivo classes.txt. Note também que, tanto executando o programa com sucesso quanto em situações de erro, a mensagem: "fechando o BufferedStream" aparece. Ou seja, o finally é executado e os recursos são liberados corretamente em ambos os casos.

Essa abordagem no uso do finally é muito comum quando trabalhamos com acesso a banco de dados, manipulação de arquivos e IO em geral, entre outras situações. São em casos como esses que é necessário que o programa adquira recursos do SO quando necessário e os libere posteriormente.

Para tirar esse trabalho do desenvolvedor, o Java 7 traz o gerenciamento automático de recursos. Observe o código a seguir que demonstra essa funcionalidade no método main(), substituindo o mesmo método do exemplo anterior (classe GerenciadorRecursosMultiCatch):

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 a mudança, a declaração e a inicialização do MyBufferedReader são feitas no próprio try. Isso determina que a instância terá seu método close() chamado automaticamente, de forma semelhante àquela obtida quando usamos o finally. Isso certamente evita confusões e erros na hora de liberar 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. Com isso, qualquer classe que implementa essa interface pode ser passada para o try, e o método close() será chamado correctamente. Em decorrência da chamada implícita do close(), esse try deve fazer parte de outro try contendo um catch para Exception; ou então o método deve lançar Exception.

Ao executar o programa de exemplo, após a modificação do método main(), será mostrada a mensagem "fechando o BufferedStream", mesmo sem a chamada explícita do método close().

Continuando com o mesmo exemplo, passamos à demonstração do recurso de multicatch, que permite a um catch tratar múltiplas excepções. Dessa forma, quando existem vários catch fazendo um mesmo tratamento para exceções diferentes, é possível juntá-los em um único catch. Utilizando a nova sintaxe no método instantiate(), o trecho relevante do exemplo fica da seguinte forma:

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, juntamos as duas exceções num só catch. Repare que as exceções são separadas por um carácter pipe, '|'. A sintaxe do multicatch é definida da seguinte forma:

try {
  // código
} catch (Exceção 1 | Exceção 2 | ... | Exceção n variável) {
  // tratamento da excepção
}

Aqui, Exceção 1 | Exceção 2 | ... | Exceção n são exceções (classes que herdam de java.lang.Exception) e variável é a variável na qual será atribuída a instância da exceção que foi lançada por alguma instrução no bloco try. A forma como é feito o tratamento da exceção no bloco exception continua igual ao que era feito antes dessa modificação de sintaxe.

Conclusões

Foram apresentadas as mudanças na linguagem Java que estarão presentes no JDK 7. Essas mudanças foram propostas pelo projecto Coin, um subprojecto do OpenJDK, através da JSR 334. Entre as mudanças propostas, as que podem ser consideradas mais significativas são o suporte a strings na expressão do switch, o gerenciamento automático de recursos e, com o objectivo de reduzir a verbosidade da linguagem, os recursos de inferência de tipos genéricos e o multi-catch. O código fonte dos programas utilizados como exemplo estão disponíveis no Github.

Apesar de o itens apresentados nesse estarem de acordo com o que foi sugerido na versão actual da JSR 334, e também em conformidade com a implementação disponível no OpenJDK, é possível que ainda ocorram pequenas mudanças até a finalização da especificação.

Sem comentários:

Enviar um comentário