Não posso descobrir nem instanciar tipos genéricos! Porque?

São incontáveis os posts no GUJ com uma pergunta semelhante: “Posso extrair o nome de um tipo genérico?“, “Não consigo extrair tipo do genérico!“, “Utilizando generics para instanciar objetos“, entre outros. Curioso que esse tipo de pergunta tem aumentado muito nos últimos tempos, identificando um possível crescimento no uso do Java 5 em diante. Já era a hora!

Eu já havia postado sobre isso quando falei de reificação de tipos parametrizados, mas de uma maneira mais geral.

A questão básica é a seguinte: Se eu tenho um tipo genérico, que recebe um tipo parametrizado T como argumento, eu posso instanciar T de alguma forma?

Quem sabe tentar assim:

class Dao<T> {
  public T cria() {
    return new T();
  }  
}

Essa forma não funciona. Você não tem garantias sobre os construtores que o tipo T possui. E então se tentarmos assim:

class Dao<T> {
  public T cria() {
    return T.class.newInstance();
  }  
}

Aqui a sintaxe até poderia ser possível, mas infelizmente o java não sabe quem é T nem mesmo em tempo de execução. Nem mesmo com manipulação de bytecode ou qualquer outro recurso. Isso porque o compilador “apaga” essa informação depois de utilizada: é a tal da mal falada erasure.

Qual seria a vantagem da erasure?

Antes de mais nada: erasure não serve para poder rodar código do Java 5 em VMs Java 1.4 ou menor! Não é esse o objetivo.

Quando a JSR14 do generics foi proposta, eles queriam mais que compatibilidade para trás em relação a compilação, eles queriam também a possibilidade de migrar o código antigo para poder usar código novo: uma ArrayList precisa poder ser passada como argumento para alguém que receba List<QualquerClasse> como argumento!

Para ilustrar a situação, imagine que eu tenho uma aplicação grande X que usasse a classe ArrayList em muitos lugares (como a grande maioria das aplicações). Essa aplicação X usa a biblioteca B, que recebe List como argumento em muitos de seus métodos. Se um dia a bibioteca B passasse a usar List genérica, gostaríamos que a aplicação X atualizasse B sem maiores problemas: sem precisar recompilar nada, nem trocar nada no código fonte. O Neal Gafter escreveu muito sobre isso pouco antes da release do Java 5, dada as inúmeras críticas que eles estavam recebendo.

Se o Java tivesse optado por outra maneira de implementar generics, teria ou quebrado compatibilidade com o uso não genérico de classes que viraram genéricas, ou precisaria criar classes paralelas as atuais, praticamente copiadas e coladas, só que estas genéricas, tornando as antigas deprecated ou legadas.

Como o .NET resolveu o mesmo problema?

O .NET seguiu essa segunda forma, e criou uma hierarquia quase que paralela de coleções dentro do name space System.Collections.Generic.

A IList é a interface que define as operações em uma lista não genérica, e a ArrayList é sua implementação mais comumente usada. Quando entrou generics no .NET eles criaram uma outra interface para a lista, com mesmo nome, só que genérica: a IList<T>.

A classe List já é a implementação da interface genérica, e é ela quem você vai usar em vez de ArrayList. Ela possui uma definição bem estranha:

public class List : IList, ICollection, IEnumerable, IList, ICollection, IEnumerable

Ela implementa tanto a lista genérica como a não genérica. O .NET tem um recurso que nós não temos, que faz com que apesar dessa lista também implementar a interface não genérica, você só consegue invocar os métodos não genéricos (que trabalham com Object) se estiver se referenciando a ela explicitamente como uma lista não genérica, como o código abaixo:

IList x = new List<String>();
x.Add(2);

Apesar desse código compilar, no .NET temos essa informação dos tipos genéricos em tempo de execução, o que fará gerar uma exceção:

System.ArgumentException: The value "2" is not of type "System.String" e cannot be used in this generic collection

No Java teríamos apenas um unchecked warning na linha da declaração da referência, e uma possível ClassCastException mais a frente no código.

Aqui a vantagem é você poder passar uma List genérica para um código .NET antigo, que recebe como argumento uma IList não genérica. Além disso, no .NET você pode sim descobrir quem é T em tempo de execução:

class ClasseGenerica<T> {
  void metodo() {  Console.WriteLine(typeof(T)); }
}

Em resumo: o sistema de generics do .NET é realmente seguro, não temos como burlá-lo através de unchecked casts, como ocorre em Java. O Java novamente sacrificou alguns recursos interessantes em favor a compatibilidade de versões e interoperabilidade entre classes genéricas e as não genéricas já existentes. Cada um com sua vantagem. Como citei no outro artigo, existem algumas idéias de dar suporte a tipos genéricos reificados no Java, ao mesmo tempo que outros ficariam ainda com a erasure, sendo que você pode escolher qual o que te agrada para aquela classe genérica em particular. Talvez ter as duas opções adicione ainda mais complexidade a tipagem genérica do Java, mas eu particularmente gosto da idéia.

Mais uma vez um post que era para ser sucinto ficou longo. Agradeço ao Rafael Steil e Rodrigo Kumpera pela colaboração, e ao Lucas Cavalcanti e Guilherme Moreira pedindo para que fosse elaborado um post mais completo sobre esse assunto tão pertinente.

17 Comentários

  1. Diego Carrion 28/04/2008 at 23:09 #

    Excelente o post Paulo, bem informativo e simples de entender como sempre.

  2. Tiago Albineli Motta 29/04/2008 at 16:01 #

    Interessante. Não havia passado por esse problema ainda, mas é bom saber desde antes.

  3. Gabriel 06/05/2008 at 22:28 #

    O exemplo em Java não é muito feliz. Não vejo motivo prático para alguém tentar instanciar uma cópia de uma classe parametrizada através dela mesma. Por outro lado, é possível fazer o mesmo através de um parâmetro:

    public class Copier {
    public static T copy(T obj) throws Exception {
    return (T) obj.getClass().newInstance();
    }
    }

    Além disso, se alguém está tentando brincar muito com os tipos, provavelmente está fazendo algo errado; isso tende a levar justamente a ifs que a herança e o polimorfismo removeram (todo mundo conhece o exemplo das formas geométricas).
    Para terminar, tendo trabalhado com .NET também, eu diria que dividir a hierarquia em dois ramos é uma má idéia, porque complica desnecessariamente a API. Neste ponto, acho que a Sun acertou. Como minha linguagem favorita é o Perl, posso me declarar isento nessa discussão.

  4. Gabriel 06/05/2008 at 22:30 #

    O sistema comeu o parâmetro da assinatura:
    public static <T> T copy(T obj) throws Exception {

  5. Paulo Silveira 06/05/2008 at 22:52 #

    Gabriel. O exemplo foi puramente ilustrativo, mas existem casos que seria muito interessante dar new em uma array de T. No seu exemplo voce tem um objeto do tipo T, mas imagine o metodo busca() de um DAO, onde voce nao tem um T para pegar o getClass em tempo de execucao? Nao da…

    Não faz sentido algum o DAO criar um objeto T, mas como disse faz todo o sentido precisar da classe de T para invocar o metodo load/find da Session/EntityManager do Hibernate/JPA. Nesse caso você precisa do tipo reificado.

    Como no Java não há, faz-se necessario passar a classe de T para o DaoGenerico, o que fica parecendo bem redundante ao se ler o código.

    Eu me simpatizo com a implementação da Microsoft, mas como não tenho experiência no desenvolvimento .NET, pode ser apenas o efeito de que a “grama do vizinho é mais verde”.

  6. Paulo Silveira 06/05/2008 at 22:56 #

    Vale a pena dar uma olhada na sugestao de GenericDao do hibernate:
    http://www.hibernate.org/328.html

    Usar esse truque de ter de estender a classe eu acho muito antinatural, até comentei dele aqui:
    http://blog.caelum.com.br/2006/10/29/brincando-com-generics-o-bizarregenericdao/

  7. Gabriel 06/05/2008 at 23:47 #

    A solução da herança é boa e eu uso bastante. Por sinal, comecei a usá-la em .NET antes de aplicá-la no Java. Gosto como ela torna o código mais limpo (principalmente as instanciações). Mas insisto que esses joguinhos para descobrir qual o tipo usado num generic tendem a revelar que algo está mal no código. Exceto por algum tipo de framework, a maior parte do código simplesmente não precisa usar isso, nem qualquer forma de reflexão. Se os tipos estão limitando ou sujando o projeto, talvez seja mais natural usar uma linguagem dinâmica, na qual isso seja natural, como Perl.

  8. ronildo braga 08/05/2008 at 17:38 #

    Paulo

    Ei li os seus artigo, e depois li o artigo do GenericDao do hibernate e me ficou a impressão que não é possivel criar um método generico para buscar todos os registros.

    A solução do hibernate me parece muito complicada, não seria mais facil fazer desta forma?
    public List listAll(T entity){
    Criteria crit = getSession().createCriteria(entity.getClass());

    }

  9. Paulo Silveira 08/05/2008 at 19:50 #

    Ola Ronildo

    Parece um tanto estranho voce ter de receber um objeto para poder listar todos aqueles objetos, nao concorda? Exemplo: Num Dao de funcionario generico, pra listar TODOS os funcionarios, eu preciso passar um como argumento.

    E como ficaria para ler um Funcionario? Precisaria receber um ID e outro funcionario!!!!

    É possivel sim buscar todos sem passar esse argumento estranho guardando o Class da classe… como no link do dao generico…

    abracos!

  10. Marcelo Manzan 16/05/2008 at 12:12 #

    Um exemplo que pode dar certo.

    public class Dao<T> {
    private Class<?> paramType;

    public Dao() {
    this.paramType = (Class<?>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }
    @SuppressWarnings("unchecked")
    public T cria() throws Exception {
    return (T) this.paramType.newInstance();
    }
    }

    Isso funciona bem, e pode ajudar a sanar algumas necessidades.
    Abraços.

  11. Paulo Silveira 16/05/2008 at 12:16 #

    Ola Mazan!

    Isso é o que esta descrito aqui:
    http://blog.caelum.com.br/2006/10/29/brincando-com-generics-o-bizarregenericdao/

    Mas pra isso voce vai precisar estender a classe… um tanto incomodo.

  12. Marcelo Manzan 16/05/2008 at 12:37 #

    É… inevitavelmente é precido estender a classe. Seria até melhor considerá-la abstrata na definição, evitando seu uso direto e obtendo erro apenas em tempo de execução. Isso pode ser incômodo, mas nem sempre…

    Abraços!

    PS.: Eu desconhecia o artigo anterior 🙂

  13. Anselmo Battisti 06/09/2008 at 09:13 #

    Parabéns foi de grande valia seu post, muito obrigado!

  14. Alexandre Saudate 02/10/2008 at 06:41 #

    Faço coro com o comentário do Paulo Silveira. O JBoss Seam utiliza uma estrutura parecida para gerenciar entidades persistentes. Se quiserem saber do que estou falando, vejam a classe EntityHome, do Seam.

  15. Alexandre Saudate 02/10/2008 at 06:41 #

    Corrigindo, comentário do Marcelo Manzan

Deixe uma resposta