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.