Você está na página 1de 7

Um passeio pelo Java 5

Parte II – Generics
por Christian Cleber Masdeval Braz

Introdução
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 seqüê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
Um passeio pelo Java 5
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.

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);
}
}
Um passeio pelo Java 5
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>();


c.add(new Object()); //Erro de compilação
void add (G var)
irá falhar miseravelmente. Isso porque no método add { var será do tipo ?, o
que representa “qualquer tipo” e como o compilador não array[i] = var; consegue definir qual,
também não permite que a atribuição seja feita. }

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

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.
Um passeio pelo Java 5
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:
Um passeio pelo Java 5
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).ê

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

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.

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
Um passeio pelo Java 5
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) {…}

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
Um passeio pelo Java 5
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;
}

Conclusões
As conclusões devem fornecer um fechamento breve e claro para o artigo.
Fulano de Tal (fulanotal@provedor.com.br) é desenvolvedor certificado Delphi. Trabalha com WebSnap e DataSnap e
desenvolvimento wireless em projetos de integração com soluções enterprise, usando Oracle, BES e Caché.