Migrando para o Java 5: Tipos Genéricos
Artigo Java Magazine 28 - Migrando para o Java 5
Saiba como incorporar a mais sofisticada novidade de linguagem do J2SE 5.0 nos seus sistemas, começando com um tutorial completo.
Artigos Java Artigo Java Magazine 28 - Migrando para o Java 5
Migrando para o Java 5<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
Parte 2: Tipos Genéricos
Como incorporar a mais sofisticada novidade de linguagem do J2SE 5.0 nos seus sistemas, começando com um tutorial completo
O suporte a tipos genéricos é a principal novidade de linguagem do Java <?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />5, a mais evidente e mais comentada – e um dos maiores incentivos para a migração para a nova versão. Por outro lado, também será o recurso que levará mais tempo para ser utilizado no seu pleno potencial: não só corretamente, mas da forma mais eficaz e elegante, em todos os casos em que pode contribuir para a melhoria de um software.
O artigo Fazendo a Migração nesta coluna conclui a série iniciada na edição anterior, com dicas e orientações práticas para a adoção deste poderoso recurso do novo Java. Mas para quem ainda não está familiarizado com os genéricos, começamos com um Tutorial que apresenta todos os conceitos básicos necessários, e revisita o tema para incluir tópicos mais avançados do que os já explorados em artigos anteriores sobre genéricos
Tutorial de Genéricos
Tipos genéricos devem ser vistos antes de mais nada como uma evolução natural da linguagem Java. Isto porque levam adiante e mais a fundo princípios que existem desde a primeira versão da tecnologia. O mais importante destes princípios é o sistema estático de verificação de tipos, idealizado para detectar erros como conversões ilegais, ou a invocação de métodos inexistentes num objeto em tempo de compilação. Isto é muito mais robusto do que permitir a compilação e só muito mais tarde, ao executar o trecho de código incorreto, ocorrer uma exceção ou outro comportamento indesejado.
Cada linguagem naturalmente tem suas vantagens, e algumas pessoas discordam que tipos estáticos sejam uma boa idéia. Para quem acha que declarar o tipo de todas as variáveis dá mais trabalho do que traz benefícios, existem linguagens dinamicamente tipadas. Mas se você escolheu Java, certamente concorda que os tipos estáticos são boa coisa: afinal, trata-se de uma das características mais fundamentais da tecnologia. Gostar do Java e não gostar de tipos estáticos é como gostar de ópera e não gostar da língua italiana.
Os tipos genéricos nada mais são que os tipos estáticos levados às últimas conseqüências. A idéia é que deveríamos incluir, em declarações estáticas, ainda mais informações de tipo. Por exemplo, uma linguagem sem tipagem estática declararia algo como "var documentos", negando ao compilador qualquer informação sobre o conteúdo da variável. Já uma linguagem estaticamente tipada, como o Java "tradicional" (pré-Java 5), declararia "List documentos", informando ao compilador que tal variável é uma lista – o que permite validar determinadas operações (como invocação de métodos, atribuições e conversões). Finalmente, no Java 5, podemos declarar "List<Documento> documentos", o que fornece ao compilador ainda mais detalhes sobre o uso dessa variável. Agora, o compilador conhece também o tipo de todos os elementos que podem ser adicionados à lista, o que permite realizar ainda mais verificações automáticas.
O contrato dos tipos genéricos
Na linguagem Java tradicional, o sistema de tipos garante que nenhuma conversão ilegal seja feita. Todas as conversões implícitas (que não exigem typecasts) funcionam garantidamente, sem jamais gerar problemas. Já para as conversões explícitas – forçadas com typecasts – o Java garante que as únicas possibilidades sejam que ou a conversão funciona ou gera uma ClassCasException, mas que nunca causa qualquer outro problema (como corrupção da memória, core-dumps/crashes etc.).
Com os tipos genéricos, a linguagem pode dar garantias ainda mais fortes. Segundo o que podemos chamar de princípio fundamental dos tipos genéricos, um programa que não possui nenhum typecast e que não gera nenhum warning de compilação é type-safe, ou seja, jamais irá gerar uma ClassCastexception.
Mas note o requisito dos warnings. Com o javac, você deve compilar com javac –Xlint:unsafe (ou –Xlint:all): esta opção gera warnings para determinadas operações inseguras que o Java 5 tolera por motivo de compatibilidade, e que tipicamente são necessárias para conviver com código legado (veja mais no tópido "Tipos brutos").
Na essência dos tipos genéricos, temos o recurso de parâmetros de tipo, que podem ser aplicados tanto a interfaces e classes quanto a métodos:
class Par<C, V> {
private C chave;
private V valor;
Par (C chave, V valor) {this.chave = chave;this.valor = valor;}
void setValor (V valor) {this.valor = valor;}
V getValor () {return valor;}
void setChave (C chave) {this.chave = chave;}
C getChave () {return chave;}
Par<Interger,String> p = new Par<Interger,String>(55, "Brasil");
String pais = p.getValor();
p.setChave("Brasil");//Erro!
A declaração Par<C, V> define um tipo genérico Par, ou seja, um tipo com pelo menos um parâmetro de tipo (no caso temos dois: C e V). Dentro da classe Par, podemos usar os mesmos parâmetros de tipo nas declarações de variáveis locais, atributos, parâmetros, e retorno.
Quando declaramos Par<Interger, String> p, usamos um tipo parametrizado – que teve seus parâmetros de tipo satisfeitos, no caso pelos tipos Interger (argumento para C) e String (para V). Esta instanciação de tipo genérico para tipo parametrizado pode ser compreendida como uma simples "busca e substituição" no código da classe genérica. É como se tivéssemos criado uma cópia da classe Par com um atributo Interger chave, um método void setChave(Interger chave) e assim por diante. Vistos dessa forma, os tipos genéricos são simples. Basta habituar-se à nova sintaxe.
Métodos individuais podem definir seus próprios parâmetros de tipo:
static <T> T Max (T a, T b) {
return a.compareTo(b)>=0? A:b;
Neste exemplo, temos um método Max() que retorna o maior de dois objetos (de acordo com o critério de comparação definido em compareTo()). O parâmetro de tipo T, que aqui garante que os parâmetros a e b e o retorno sejam todos do mesmo tipo, não é definido por uma classe, mas pelo próprio método.
E como parametrizamos este método? Ao contrário dos tipos genéricos, que exigem a sintaxe com <>, não precisamos fazer nada diferente para usar os métodos genéricos. Basta fazer invocações comuns, como Max("João","Maria"). O compilador irá verificar que os parâmetros passados são do tipo String; portanto temos neste caso que T = String.
Interferência de tipos
Continuando o exemplo, e se você fizesse uma invocação como a seguinte?
Number n = max(new Interger(5), new Double(3.1));
Isto parece illegal, pois a assinatura de Max() exige que os seus dois parâmetros sejam de um mesmo tipo T. mas como o Java suporta conversões implícitas, o compilador só exige que a e b sejam compatíveis com algum tipo em comum que satisfaça às exigências da declaração e da invocação. Interger e Double herdam de vários tipos em comum: Number, Comparable, Object e Serializable. O valor de retorno está sendo atribuído a uma variável do tipo Number; portanto, o compilador determina que T = Number. Esta escolha é chamada inferência de tipos[1]. O compilador determina os argumentos para parâmetros de tipo automaticamente, em circunstâncias onde isso for possível e necessário.
Valendo prêmio máximo: de que tipo serão retorno de "max(newJWindow(), newJButton());"? Como o retorno não é usado, o compilador não tem como escolher um dos supertipos comuns aos parâmetros. E não escolhe mesmo. A assinatura parametrizada de Max() para esta invocação será "Container&Accessible", que significa literalmente "qualquer tipo que seja um Container ou que seja um Accessible". Tanto Jwindow quanto Jbutton herdam estes dois tipos. Portanto, o resultado desta invocação a Max() poderia ser atribuído a um Container ou a um Accessible, ou a algum outro supertipo destes (ex.: Component). A expressão "Container&Accessible" não denota um tipo, e sim o resultado de uma interferência de tipos parcial.
A interferência de tipos geralmente funciona de forma automática e silenciosa. Você só vai notar que existe quando fizer algo errado – pois nestes casos, o compilador irá gerar mensagens de erro que exibem os tiros parcialmente inferidos. Por exemplo, se tentarmos compilar:
String s = Max(new Jwindow(5), new Jbutton(3.1)
o compilador reporta:
Type mismatch: cannot convert from Coitainer&Accessible to String
Um iniciante em tipos genéricos pode ficar confuso com esta mensagem, pois o código não utiliza nenhum dos tipos Container ou Accessible – quanto mais a expressão bizarra como o &. Mas quando sabemos sobre a interferência de tipos, fica fácil diagnosticar e corrigir o problema.
Limite de tipo
Nosso exemplo Max(), até aqui, possui um erro proposital (opa!). Invocamos compareTo() sobre parâmetros declarados como T, mas este método só existe em objetos que implementam a interface Comparable. Para que o código compile, precisamos exigir que T seja compatível com Comparable. Ou mais precisamente, com Comparable<T>, pois no J2SE 5.0 esta interface também é genérica. Podemos fazer isso com ...