A última versão mais importante da linguagem Java havia sido a 1.2. De lá para cá novas versões apenas revisaram algumas características e corrigiram bugs. A versão 1.5 (Java 5 como vem sendo chamada) no entanto é outro marco! Novas características foram incluídas na linguagem tornando-a mais robusta e fácil de usar. A JVM também sofreu melhorias e está mais rápida e confiável.

Irei neste artigo introduzir as principais mudanças ocorridas na linguagem. Assim como o pincel é para o artista, a linguagem de programação é nossa principal ferramenta. Conhecê-la bem e saber utilizar adequadamente seus recursos é importante para produzirmos código de qualidade.

Nas seções que seguem irei abordar os temas: laço for melhorado, Autoboxing/Unboxing, enumerações (Enums), métodos com número de argumentos variável (Varargs) e Static Import. No próximo artigo falarei sobre a criação de classes e métodos parametrizados (Generics), a mais importante (e complexa) das alterações na linguagem.

Melhoria no laço for

A primeira modificação que apresento é referente ao laço for. Um exemplo da sintaxe até o Java 2 para iterar num array ou coleção usando for pode ser vista abaixo:

 //Acessando um array de inteiros 
    int[] a = {1,2,3,4}; 
    for (int i = 0; i < a.length; i++)
      {
        //utiliza os elementos do array
       }
       //Acessando uma coleção
       ArrayList<Color> b = new ArrayList<Color>(); 
       for(Iterator<Color> i = b.iterator(); i.hasNext(); )
       {
         //utiliza os elementos da coleção  
         Color c = i.next();
         ....
    }              
    

Desvantagens dessa sintaxe:

  1. É mais trabalhosa do que necessitaria ser. A variável de iteração (i nos exemplos) ocorre três vezes em cada laço. A necessidade de escrevê-la várias vezes traz maior chance de se cometer erros.
  2. Considere o código:


    1 List stores = ...;
    2 List people = ...;
    3 List clientList = new ArrayList();
       
    4 for(Iterator<Store> i = stores.iterator(); stores.hasNext(); )
    5  for(Iterator<People> j = people.iterator(); people.hasNext())
    6   clientList.add(new Client(i.next(), j.next()));
    

Você consegue perceber o erro? O problema é que o método next() é chamado mais vezes do que deveria na coleção do “outer” loop. Na linha 6 o i.next() executa o mesmo número de vezes que o j.next(), o que é um erro de lógica. Uma forma de corrigir o problema é acrescentando uma variável intermediária para fixar o valor do “outer” loop:

 for(Iterator<Store> i = stores.iterator(); stores.hasNext(); )
    { 
       Store aux = stores.next();
       for(Iterator<People> j = people.iterator(); people.hasNext())
        method1(aux, j.next());
     }

Com o novo for o código acima pode ser escrito da seguinte maneira:


     //Acessando um array de inteiros
    int[] a = {1,2,3,4}; 
    for (int i : a)
    {
      //utiliza os elementos do array
    }
       
    //Acessando uma coleção
    ArrayList<Color> b = new ArrayList<Color>(); 
    for(Color c : b)
    {
     //utiliza os elementos da coleção
    }  

O “:” pode ser lido como “em” e o laço todo como “para cada cor c em b”.

Vantagens:

  1. Código mais simples e claro, conseqüentemente menos suscetível a erros.
  2. O problema descrito acima, no item 2, passa a não existir. O novo código fica:

for(Store store : stores)
    for(People p : people)
    clientList.add(new Client(store,p));
    

Limitações:

  • Como um iterator não é declarado com o novo for não é possível escrever código para remover ou acrescentar elementos na coleção.
  • Pelo mesmo motivo anterior, não é possível iterar em mais de uma coleção paralelamente.

Autoboxing

Como sabemos, não é possível atribuir diretamente um primitivo a um objeto de uma das classes wrapper ou mesmo para um objeto do tipo Object. Sendo assim, para atribuir um int a um Integer, fazemos:

 Integer num = new Integer(10);
    Object num = new Integer(10);
    

e para atribuir um Integer a uma variável do tipo int:

 int x = num.intValue();

Também não é possível inserir primitivos em coleções; pode-se inserir apenas objetos. Quando há a necessidade de inserir algum tipo primitivo, isso deve ser feito através de uma das classes de wrapper. Por exemplo:


    List<Integer> lista = new ArrayList<Integer>();
    lista.add(new Integer(10));
    

O novo mecanismo de autoboxing elimina a distinção entre um primitivo e um objeto permitindo que se faça a atribuição entre ambos livremente (respeitando-se é claro os tipos). O Java 5 automaticamente converte, quando necessário, tipos primitivos de um determinado tipo para seu wrapper equivalente (box) e vice-versa (unbox). Portanto, os códigos acima ficam da seguinte forma:

Integer num = 10;                          
    //10 é convertido para Integer
    Object num = 10;                                            
    //10 é convertido para Integer
    int x = num;                                 
    //num é convertido para int
    List<Integer> lista = new ArrayList<Integer>(); 
    lista.add(10);                                
    //10 é convertido para Integer
    

O efeito disso é que em qualquer parte do código podemos tratar os primitivos e seus wrappers indistintamente. Observe mais um trecho de código:

  Boolean canMove = new Boolean(true); 
    if(canMove)
    { 
     System.out.println("This code is legal in Java 5!"); 
    }
    

O comando if espera por um primitivo do tipo boolean mas recebe um objeto Boolean. Não tem problema! O compilador irá fazer um unbox e gerar o código apropriado.

Enumerações

Uma enumeração é um tipo especial de classe que cria um novo tipo de dado com domínio pré-definido. Considere a declaração,

enum Season {WINTER, SPRING, SUMMER, FALL}

a palavra reservada enum foi adicionada e permite definir tipos enumerados. Um tipo enumerado pode assumir apenas valores previstos e, ao mesmo tempo que possue um valor numérico, possue também um textual. Observe o código abaixo:


    1 Season s = Season.WINTER;                          
    2 System.out.print(s);                       //imprime WINTER
    3 System.out.print(s.ordinal());          //imprime 0
    4 s = Season.SPRING;                                   
    5 System.out.print(s);                       //imprime SPRING 
    6 s = 3;                                          //erro pois espera receber algo do tipo Season
    7 for(Season aux : Season.values())
    8   System.out.print(aux);                 //imprime todos os valores da enumeração Season
    

O tipo Season define as quatro estações do ano – WINTER, SPRING, SUMMER e FALL – enumerados de 0 a 3. Uma variável declarada como sendo do tipo Season pode assumir apenas valores deste domínio (typesafe). Para acessar o valor textual basta chamar o método toString() e para acessar o numérico o método ordinal(). Na linha 7 o método estático values() é chamado para retornar todos os valores de uma enumeração num array. Pode-se ainda adicionar dados e comportamentos a uma enumeração. O exemplo abaixo ilustra isso.


    public enum Planet
    {
      MERCURY (3.303e+23, 2.4397e6),
      VENUS   (4.869e+24, 6.0518e6),
      EARTH   (5.976e+24, 6.37814e6),
      MARS    (6.421e+23, 3.3972e6),
      JUPITER (1.9e+27,   7.1492e7),
      SATURN  (5.688e+26, 6.0268e7),
      URANUS  (8.686e+25, 2.5559e7),
      NEPTUNE (1.024e+26, 2.4746e7),
      PLUTO   (1.27e+22,  1.137e6);
       
     private final double mass;   // in kilograms   Variáveis de instância para cada um dos planetas.
     private final double radius; // in meters
       
    Planet(double mass, double radius) 
    {
      this.mass = mass;
      this.radius = radius;
    }
       
       
    public double mass()   { return mass; }
    public double radius() { return radius; }
       
    //universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;
       
    public double surfaceGravity()  
      {
        return G * mass / (radius * radius);
     }
       
     public double surfaceWeight(double otherMass)
     {
         return otherMass * surfaceGravity();
     }
    }

Cada planeta conhece sua massa e seu raio (mass e radius) e pode calcular a gravidade (surfaceGravity) em sua superfície e o peso (surfaceWeight) de um objeto no planeta. O código abaixo recebe o peso de algum objeto na Terra e calcula e imprime este peso nos demais planetas.

    public static void main(String[] args) 
          {
              double earthWeight = Double.parseDouble(args[0]);
       
              double mass = earthWeight/Planet.EARTH.surfaceGravity();
       
              for(Planet p : Planet.values())
                 System.out.printf("Your weight on %s is %f%n",p, p.surfaceWeight(mass));
          }
       
        $ java Planet 175
       
        Your weight on MERCURY is 66.107583
        Your weight on VENUS is 158.374842
        Your weight on EARTH is 175.000000
        Your weight on MARS is 66.279007 // O método executa  corretamente para o  planeta em questão.  
        Your weight on JUPITER is 442.847567
        Your weight on SATURN is 186.552719
        Your weight on URANUS is 158.397260
        Your weight on NEPTUNE is 199.207413
        Your weight on PLUTO is 11.703031
    

Por fim, é possível ainda criar métodos particulares para cada uma das constantes de uma enumeração. Para isso, pode-se declarar um método abstrato e sobreescrevê-lo com um método concreto em cada uma das constantes. Segue um exemplo dessa abordagem.

  
    public enum Operation {
      PLUS   { double eval(double x, double y) { return x + y; } },
      MINUS  { double eval(double x, double y) { return x - y; } },
      TIMES  { double eval(double x, double y) { return x * y; } },
      DIVIDE { double eval(double x, double y) { return x / y; } };
       
       // Do arithmetic op represented by this constant
       abstract double eval(double x, double y);
    }

Varargs

Em versões passadas da linguagem, para que um método pudesse receber um número arbitrário de valores era necessário passá-los num array como argumento do método. Ainda é verdade que múltiplos argumentos são passados em um array, mas no Java 5 isso é automatizado e transparente para o usuário. O método format da classe MessageFormat, por exemplo, possui a seguinte declaração:

public static String format (String pattern, Object... argumentos);

Os três pontos no final do último parâmetro indica que o último argumento pode ser passado como um array ou uma sequencia de valores. Varargs podem ser usados apenas como último argumento de um método.

Static Import

Para acessar membros estáticos de uma classe é necessário qualificar as referências com o nome da classe onde estes estão declarados. Veja o código abaixo por exemplo,

 double r = Math.cos(Math.PI * theta);

os elementos estáticos da classe Math só podem ser referenciados através do nome da classe. A importação estática permite acesso direto a estes da seguinte forma:

 import static java.lang.Math.PI;

para disponibilizar apenas um elemento, ou

 import static java.lang.Math.*;

para disponibilizar todos os elementos estáticos de uma classe. Uma vez feito isso, o código inicial pode ser escrito da seguinte forma:

 double r = cos(PI * theta);

Algumas considerações devem ser observadas na utilização do static import. Utilize este artifício apenas quando for requerido acesso muito freqüente a membros estáticos de uma ou duas classes. A utilização em excesso poluirá o espaço de nomes do seu programa e tornará seu código menos legível. Ficará difícil distinguir a qual classe uma variável estática pertence. A importação de todos os membros estáticos de uma classe é particularmente problemático; se você precisa apenas de um ou dois elementos importe-os individualmente.

As inovações da linguagem Java permitem a criação de código mais simples, legível e eficiente. Conhecê-las e utilizá-las da forma correta representa um diferencial significativo para a construção de programas com maior qualidade. No meu próximo artigo, abordarei a utilização de classes genéricas – sem dúvida o diferencial mais importante e poderoso da nova Java 5.

Mais sobre Java