cup_generics Trabalhar com genéricos é uma abordagem muito produtiva para reutilização de código.

Exemplo: DAOs genéricos

Implementações genéricas de DAOs (Data Access Object) é algo que vejo repetidas vezes em diferentes projetos e equipes. Implementando alguns métodos genéricos, é possível evitar a criação de dezenas de classes semelhantes num projeto. Se bem planejado e usado, técnicas como essa economizam tempo e facilitam a manutenção.

Em Java, podemos criar uma classe base com um tipo genérico para representar nosso DAO. Considere o seguinte trecho de código:

public class BaseDao<T, ID> {
    public T find(ID entityId) { /*...*/ return null; }
    public void insert(T entity) { /*...*/ }
    public int update(T entity) { /*...*/ return 0; }
    public void delete(ID entityId) { /*...*/ }
}

Há duas principais abordagens para usar uma classe genérica como essa.

Herança

Podemos estender a classe BaseDao, gerando subclasses específicas que estendem as funcionalidades padrão.

Exemplo:

public class ClienteDao<Cliente, Integer> { ... }

public class LoginDao<Login, String> { ... }

Uso direto

Quando não há necessidade de especialização, é possível usar diretamente a classe base.

Exemplo:

BaseDao<Cliente, Integer> baseDao = new BaseDao<>();

BaseDao<Login, Integer> loginDao = new BaseDao<>();

Identificando o tipo genérico

Em determinadas situações é desejável descobrir em tempo de execução qual é o tipo genérico de uma classe.

Continuando em nosso exemplo do DAO, poderíamos ler alguma anotação ou configuração associada à classe Cliente quando o DAO fosse deste tipo.

Como fazer isso nos dois tipos de DAOs mencionadosacima?

Herança

Considere o código:

public class BaseDao<T, ID> {

    final private Class<T> entityType;

    public BaseDao() {
        entityType = (Class<T>) ((ParameterizedType)
                getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    //...

}

Supondo que existem subclasses, tal como ClienteDao extends BaseDao<Cliente, Integer>, então o atributo entityType receberá a referência para a classe Cliente.

Trechos de código semelhantes a esse estão disponíveis em várias postagens e respostas do Stack Overflow, mas é algo que pode ser considerado “inseguro”, pois há cenários em que isso simplesmente não funciona.

Uso direto

O código do construtor, como exibido acima, não funciona numa instanciação direta de BaseDao.

O seguinte exemplo vai gerar um erro:

 BaseDao<Login, Integer> loginDao = new BaseDao<>(); //não vai instanciar o objeto

Qual o motivo?

Type erasure

Primeiro, é preciso entender que a informação do tipo genérico de uma variável é removida em tempo de execução.

Por exemplo:

List<String> lista = new ArrayList<String>();

O compilador “garante” que você só consiga adicionar objetos do tipo String dessa lista, mas quando sua classe é executada na JVM, não há qualquer verificação quanto ao tipo.

Sim, você pode dar um cast inseguro de List<String> para List e então adicionar outro tipo de objeto. O compilador vai aceitar, afinal ele “confia” em você.

É impossível para a instância lista saber o seu próprio tipo genérico, já que no fim das contas só existe uma única classe ArrayList, o tipo genérico é apenas um mecanismo de segurança para o desenvolvedor.

Li uma vez que manter o type satefy implicaria em um overhead muito grande para a máquina virtual, pois o Java precisaria praticamente criar uma cópia da classe List para cada tipo genérico usado no programa, de modo a ter literalmente uma versão da classe para cada tipo.

Quando é possível recuperar o tipo genérico

Repare no método getGenericSuperclass(). Ele retorna os tipos que a classe atual define para os parâmetros genéricos da superclasse imediata. Algo análogo ocorre com o getGenericInterfaces().

Isso significa que se você tiver uma subclasse assim:

class ClienteDao extends BaseDao<Cliente, Integer> { ... }

O código funcionaria perfeitamente, pois você está definindo um tipo para o parâmetro genérico da superclasse. Isso funciona porque o tipo genérico não está apenas numa variável, mas declarando de forma estática (fixa) na própria definição da classe.

Mas como você instancia diretamente o GenericDao, não há uma generic superclass e, portanto, a exceção sendo lançada.

Basta usar herança?

Usar sempre herança como no exemplo com extends é uma saída, mas muitos não recomendam porque há chances do código “quebrar” com novos erros estranhos.

Essa é uma possibilidade, pois como a própria documentação do método getGenericSuperclass() afirma, ele só retorna o tipo genérico implementado na superclasse imediata do seu DAO. Então se houver uma hierarquia diferente de exatamente dois níveis, o código vai falhar.

Recomendação

A recomendação que vai funcionar de forma mais simples e direta é passar a classe desejada por parâmetro para o construtor do GenericDao.

Exemplo:

public class GenericDao<T extends Serializable> {

    private final Class<T> entityType;

    public GenericDao(Class<T> entityType) {
        this.entityType = entityType;
    }

    ...

}    

Muito mais simples, certo?

O maior inconveniente é a chamada verbosa de criação da classe:

BaseDao<Cliente> clienteDao = new BaseDao<Cliente>(Cliente.class);

Na verdade, a partir do Java 7 poderíamos omitir o tipo Cliente da classe instanciada:

BaseDao<Cliente> clienteDao = new BaseDao<>(Cliente.class);

Melhorou um pouco, mas podemos fazer melhor. Que tal criar um método factory?

Exemplo:

public class BaseDao<T> {

    final private Class<T> entityType;

    public static <X> BaseDao<X> getInstance(Class<X> entityType) {
        return new BaseDao<X>(entityType);
    }

    private BaseDao(Class<T> entityType) {
        this.entityType= entityType;
    }

    ...

}

E a criação da instância da classe fica assim:

GenericDao<Cliente> clienteDao = BaseDao.getInstance(Cliente.class);

Alguns podem achar demais, porém considero este um design mais claro.

Considerações

Certamente há formas ainda mais flexíveis de trabalhar com genéricos usando Injeção de Dependência, Spring Beans e outros, mas por agora, estes fogem ao escopo do artigo.

O importante é compreender que Generics é um recurso importante e flexível da plataforma Java, com aplicações diretamente em nosso dia-a-dia.