Um passeio pelo Java 5 - Generics

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (1)  (2)

Neste artigo conheceremos os recurso de Generics do Java 5.

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.

No artigo passado falei sobre: laço for melhorado, Autoboxing/Unboxing, enumerações (Enums), métodos com número de argumentos variável (Varargs) e Static Import. Nesta sequência, apresento a mais significativa das mudanças ocorridas, que é a possibilidade de se criar classes e métodos parametrizados (semelhante às templates do C++). Uma classe ou método paramétrico está apto a ser invocado com tipos diferentes. Sendo assim, é possível definir uma variável dentro de uma classe ou o argumento de um método como um tipo paramétrico e apenas quando estes forem efetivamente utilizados este tipo será definido pelo usuário. Esta é uma tremenda flexibilidade para codificação e possibilita criar soluções mais genéricas e resumir muito código, o que significa menos necessidade de manutenção. Toda a API padrão da linguagem (todas as classes que implementam coleções por exemplo) foi refeita para tirar proveito destas facilidades.

O conceito de classe genérica (generics) é simples porém os detalhes para sua correta utilização são muitos. Espero ao final desse texto ter conseguido explicar de forma clara seus principais pontos.

Classes Genéricas

Considere o código abaixo:

 
public interface List<E>
{
  void add(E x);
  Iterator<E> iterator();
}
   
public interface Iterator<E>
{
  E next();
  boolean hasNext();
}

As duas interfaces são definidas com o auxílio de parâmetros de tipos formais (formal type parameters), evidenciados pelas letras embutidas entre os sinais <>. Tipos paramétricos podem ser usados no código genérico de forma muito semelhante aos tipos convencionais. Um exemplo de utilização do código acima pode ser:

 
List<Integer> myList = new LinkedList<Integer>();
myList.add(new Integer(1));
Integer x = myList.iterator().next();

Até o Java 4 existiam apenas listas de Objects e para recuperar qualquer coisa diferente disso era necessário fazer um type cast. Agora pode-se definir a priori qual o tipo dos dados que uma lista (ou qualquer outra coleção) irá armazenar. Uma lista de inteiros receberá apenas inteiros, nada além disso. As conseqüências disso são várias:

  • Não é preciso fazer cast para recuperar elementos já que o tipo correto de retorno já está especificado.
  • O código fica mais robusto pois agora o compilador pode checar os tipos envolvidos (type safe) o que não acontecia quando um cast era necessário.
  • Menos erros em tempo de execução.
  • Exibe naturalmente um certo grau de reaproveitamento de código já que códigos genéricos são feitos para serem utilizados com tipos diferentes de dados.

Um ponto muito importante sobre generics está relacionado à subtipos. Considere o exemplo:

 List<String> ls = new ArrayList<String>();
  List<Object> lo = ls;

O que você acha do código acima? Já que String deriva de Object parece razoável supor que uma lista de strings também é um subtipo de uma lista de objetcs e sendo assim a atribuição acima estaria correta. Pois bem, considere esta continuação:

lo.add(new Object());
 String s = ls.get(0);

Na primeira linha um Object foi inserido na lista e na segunda este foi recuperado e atribuído a uma String, o que é um erro! Quando fizemos a variável lo receber ls esta deixou de ser uma lista apenas de strings e abriu a possibilidade de inserirmos qualquer tipo de objeto. Definitivamente este não é o tipo de comportamento que esperamos! É claro que atribuições como a acima não são permitidas e o compilador irá emitir uma mensagem de erro quando encontrá-las.

Dica - Se Circle é uma subclasse (ou subinterface) de Shape e G é uma declaração genérica qualquer então não é verdade que G<Circle> é também um subtipo de G<Shape>. Esta é uma noção inicialmente difícil de absorver pois vai contra nossa intuição natural.

Circle

Wildcards

A decisão acima é muito restritiva. Considere a necessidade de imprimir o conteúdo de uma coleção arbitrária e a tentativa de implementá-la exposta no método abaixo:

void printCollection(Collection<Object> c)
{
  for(Object e : c)
  {
     System.out.println(e);
   }
}

Como vimos, ao contrário da intuição inicial, esse não é um método apto a receber qualquer tipo de coleção, mas sim exclusivamente coleções que contêm objetos do tipo Object. Para representar uma coleção de qualquer coisa devemos usar o caractere ? da seguinte forma: Collection<?>. Isto é chamado de wildcard type. O novo código utilizando wildcard é:

  void printCollection(Collection<?> c)
{
  for(Object e : c)
  {
     System.out.println(e);
   }
}

o qual pode ser chamado com qualquer tipo de coleção. Observe que o conteúdo da coleção c é atribuído a um Object. O wildcard é um tipo desconhecido, mas como temos certeza que será um objeto é seguro atribuí-lo a uma variável do tipo Object.

Dica

  1. Utilize wildcards sempre que precisar designar um tipo arbitrário. Porém, tenha em mente que neste caso qualquer variável genérica que for tipada com um ? será apenas para leitura. Assim sendo, o código:
    Collection<?> c = new ArrayList<String>();
    void     add (G var)
    {
      array[i] =     var;
    }
    c.add(new Object()); //Erro de compilação
    
    irá falhar miseravelmente. Isso porque no método addvar será do tipo ?, o que representa “qualquer tipo” e como o compilador não consegue definir qual, também não permite que a atribuição seja feita:
    void add (G var)
    {  array[i] = var;
    }
    
  2. A única exceção para isso é a inclusão de null.

Considere uma aplicação gráfica que desenha superfícies (shapes) tais como retângulos e círculos. Para representar esses componentes no programa podemos definir a seguinte hierarquia de classes:

shapes

Um método conveniente seria um para desenhar uma coleção de superfícies tal como:

 public void drawAll(List<Shape> shapes)
                     {
                       for(Shape s: shapes)
                       {
                         s.draw(this);
                       }
                     }

O problema aqui (novamente) é que esse método só pode ser invocado recebendo como parâmetro uma List<Shape>. Um List<Circle> por exemplo causaria erro de compilação. Isso é muito ruim tendo em vista que o objetivo do método é ler uma coleção de superfícies, sejam elas círculos ou retângulos. O que precisamos é de um método que aceite uma lista de qualquer tipo de superfície:

 public void drawAll(List<? extends Shape> shapes)
                     {
                       for(Shape s: shapes)
                       {
                         s.draw(this);
                      }
                     }

List<? extends Shape> é um exemplo de bounded wildcard. O ? representa, como antes, um tipo desconhecido. Entretanto, neste caso, sabemos que esse tipo é um Shape ou um subtipo deste. A desvantagem de não ser possível modificar uma variável com tipo desconhecido permanece.

Métodos Genéricos

Declarações de métodos também podem ser genéricas, isto é, parametrizadas por um ou mais parâmetro de tipo. Imagine como fazer um método que recebe um array de objetos e uma coleção e deve acrescentar todos os objetos do array dentro da coleção. Uma tentativa pode ser:

public void fromArrayToCollection(Object[] a, Collection<?> c)
{
   for(Object o : a)
   {
     c.add(o); //Erro de compilação!
    }
}

A opção de fazer Collection<?> para ser possível receber uma coleção arbitrária foi correta, porém, tornou a coleção c apenas para leitura. Se reformularmos este método como um método genérico podemos resolver esse problema:

 public <T> void fromArrayToCollection(T[] a, Collection<T> c)
{
   for(T o : a)
   {
     c.add(o); 
    }
 }

Podemos chamar este método com qualquer tipo de coleção cujos elementos sejam um supertipo dos elementos do array. Por exemplo:

Object[] objectArray = new Object[100];
Collection<Object> objectCollection = new ArrayList<Object>();
   
//Suponha as seguintes invocações do método fromArrayToCollection
fromArrayToCollection( Object, Object );  //Infere ser Object
fromArrayToCollection( String, Object );  //Infere ser Object
fromArrayToCollection( Integer, Number ); //Infere ser Number
fromArrayToCollection( Number, Object );  //Infere ser Object
fromArrayToCollection( Number, String );  //Erro de compilação!

Nas quatro primeiras chamadas do método o compilador pôde inferir um tipo válido para T, isto é, um tipo que é supertipo do outro. Perceba que não é necessário especificar explicitamente um tipo para um método genérico. O compilador irá inferir o tipo apropriado baseado nos tipos atuais. Na última chamada o método recebe como argumento um array de números e uma coleção de strings. Como String não é um supertipo de Number (e vice-versa) o compilador não consegue concluir um tipo válido e acusa erro.

Interoperabilidade com Código Legado

Considere a atribuição:

Collection c = new ArrayList<String>();

Ela é permitida para garantir um certo grau de compatibilidade entre as versões da linguagem (afinal todo código Java escrito até hoje foi sem generics). Quando um tipo genérico como Collection é utilizado sem parâmetro de tipo é chamado um raw type. Porém, o uso deste artifício deve ser feito com muito cuidado! Vejamos seus desdobramentos na linha abaixo:

  c.add(new Integer(10)); //unchecked warning

O compilador não acusará erro mas emitirá um aviso. O aviso é necessário porque o compilador não é capaz de garantir a corretude do resultado. Mas a possibilidade de se incluir um inteiro numa lista de strings não deveria ser um erro? Teoricamente sim, mas, na prática, se é necessário que código genérico opere com código legado isto deve ser permitido. Vejamos outro exemplo:

 ArrayList<Float> fl = c; //unchecked warning

Dica - Quando usar métodos genéricos ou wildcards?

Use wildcard quando na assinatura do método o tipo paramétrico aparecer somente uma vez, ou seja, não há interdependência de tipos (nem de retorno, nem de argumentos):

public boolean containsAll(Collection<?>  c)
public <T> boolean containsAll(Collection<T>  c)

Qual a melhor opção para os métodos acima? O parâmetro T está sendo usado neste caso apenas para prover polimorfismo e permitir que o método seja invocado com tipos diferentes. Use wildcards quando precisar representar tipos flexíveis, que é o que pretende-se expressar aqui.

“Métodos genéricos permitem que os tipos paramétricos sejam utilizados para expressar dependências entre tipos de argumentos e/ou seu tipo de retorno. Se não existe tal dependência um método genérico não deve ser empregado.”

                         
public <T> T getSomething (T     something) {…}
public static <T> void     copy (List<T> dest, List<? extends T> src) {…}

Estes métodos são exemplos de dependência do parâmetro de tipo. No primeiro, a dependência está no tipo de retorno (sim, o tipo de retorno pode ser variável !!) e no segundo a lista de origem (src) deve ser de algum subtipo da lista de destino (dest).ê

O compilador não consegue garantir que c contenha apenas objetos Float e emite novamente um aviso. Fica a cargo do programador satisfazer os contratos estabelecidos e assim, por exemplo, prezar para que c contenha uma coleção de números.

Chamar código legado a partir de código genérico é intrinsecamente perigoso! Uma vez que você mistura código genérico com código não genérico todas as garantias de segurança que usualmente um código genérico provê são perdidas. Entretanto, ainda assim é melhor do que não usar código genérico algum.

Casts, Arrays e Literais de Classe

O que o seguinte código irá imprimir?

 List<String> l1 = new ArrayList<String>();
                  List<Integer> l2 = new ArrayList<Integer>();
                  System.out.println(l1.getClass() == l2.getClass());

Você pode ficar tentado a dizer que será false mas na verdade é true. Todas as instâncias de uma classe genérica compartilham a mesma meta-classe (ou run-time class) independentemente dos valores atuais dos seus parâmetros de tipo (tal como ocorre com classes não genéricas). Uma conseqüência disso é que em variáveis e métodos estáticos, por serem compartilhados por todas as instâncias, não pode haver referências a parâmetros de tipo.

Dica - A inferência de tipos nos métodos genéricos feita pelo compilador nem sempre funciona como esperamos. Vejamos o trecho de código abaixo.

public static <T> T methodA(T[] array, T element) {…}
               
String[] sa = new String[10];
 
MethodA(sa, new String(“xx”)); //infere T = String – OK
MethodA(sa, new Integer(10)); //Consegue inferir T, mas como??

A intenção óbvia é que o tipo dos componentes do array devem bater com o tipo do segundo argumento. Por esta razão é razoável supormos que a segunda chamada será rejeitada pelo compilador, já que os tipos String[] e Integer são inconsistentes. Entretanto,não é isso que ocorre! O compilador irá inferir os supertipos comuns de String e Integer como:

T = Object&Serializable&Comparable<? extends  Object&Serializable&Comparable<?>>

que é um tipo sintético usado internamente pelo compilador e denota o conjunto de supertipos de String e Integer.

Se esta inferência irá ou não produzir o resultado desejado vai depender das circunstâncias. Porém, podemos fazer com que funcione conforme nossa intuição natural com uma pequena modificação no código:

public static <T, S extends T> T methodA(T [] array, S element) {…} 

Uma outra implicação é que não faz sentido perguntar para uma instância se esta é uma instância de um tipo particular. O código:

 Collection cs = new ArrayList<String>();
if(cs instanceof Collection<String>) {…}  //ilegal!!

é ilegal, bem como fazer os casts:

 Collection<String> str = (Collection<String>) 
cs; // unchecked warning

ou

<T> T badCast(T t, Object o) {return (T) o;} //unchecked warning

Variáveis de tipo não existem em tempo de execução. Em ambos os casos o compilador não pode garantir a conversão.

Não é permitida, por questões de segurança, a criação de arrays de tipos genéricos. Então as seguintes tentativas não são válidas:

 List<String>[] L1 = new List<String>[10]; //Erro – tentativa de criar um array de genéricos
List<String>[] L2 = new List<?>[10]; //Erro – tipos incompatíveis 
List<?>[] L3 = new List<String>[10]; //Erro – tentativa de criar um array de genéricos

A única opção possível é a criação de (unbounded) wildcard arrays. Por exemplo:

 List<?>[] lsa = new List<?>[10]; //OK - array de unbounded wildcard
lsa[0] = new ArrayList<Float>();
lsa[1] = new LinkedList<String>();
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(10));
lsa[3] = li;
Integer xx = (Integer) lsa[3].get(0); //É necessário um cast

Dica - Generics são implementados pelo compilador através de um mecanismo de conversão chamado erasure. Pode-se pensar neste como uma tradução source-to-source onde, basicamente, todas as informações de tipos genéricos são “apagadas“. Toda informação de tipo entre <> é removida, então, por exemplo, List<String> é convertido para List. Os outros usos dos parâmetros de tipo dentro do código são substituídos (geralmente) por Object. Por fim, sempre onde o código resultante não estiver com o tipo correto um cast para o tipo apropriado é inserido.

Esse funcionamento explica porque uma classe genérica é compartilhada por todas as suas instâncias e também porque operações de verificação ou conversão de tipos genéricos não funcionam.

A classe java.lang.Class é genérica agora e é um bom exemplo de aplicação de generics além de coleções. Por exemplo, o tipo String.class é Class<String> e o tipo Calendar.class é Class<Calendar>. Um dos principais benefícios é a utilização do literal de classe como fábrica de objetos e com a versão genérica isto ficou mais seguro pois pode-se obter um tipo preciso. Para entender melhor a diferença compare as duas versões abaixo:

Collection emps = sqlUtility.select(EmpInfo.class, “select * from emps”);

…
public static Collection select(Class c, String query)
{
  Collection result = new ArrayList();
 /* Executa a consulta */
 for(/* Itera no resultset */)
 {
    Object item = c.newInstance(); //Retorna um Object
    /* Utilize reflexao para setar os atributos do objeto baseado nos registros do banco*/
     result.add(item); //Povoa a coleção com Objects
  }
  return result;
}
   
Collection<EmpInfo> emps = sqlUtility.select(EmpInfo.class, “select * from emps”);
…
public static <T> Collection<T> select(Class<T> c, String query)
{
  Collection<T> result = new ArrayList<T>();
  /* Executa a consulta */
   for(/* Itera no resultset */)
    {
      T item = c.newInstance(); //Retorna um EmpInfo
      /* Utilize reflexao para setar os atributos do objeto baseado nos registros do banco*/
      result.add(item); // Povoa a coleção com o tipo específico
    }
 return result;
}

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?