Este artigo tem como objetivo introduzir o conceito de Generics da linguagem Java, mostrando exemplos reais e de fácil compreensão do mesmo, vamos abstrair o máximo possível os conceitos mais complexos, focando no real funcionamento deste poderoso recurso.
O recurso conhecido por Generics foi introduzido na linguagem Java na sua versão 5 e trouxe consigo mais robustez, segurança e qualidade aos softwares desenvolvidos com este recurso, vamos ver mais a frente o porque.
Estudando Generics
O Generics é delimitado pelos caracteres “<>”, ou seja, quando houver esse par de caracteres em uma parte qualquer do código, significa que o Generics está sendo utilizado. Vejamos um exemplo simples na Listagem 1.
List<String> listaDeStrings = new ArrayList<String>();
Vamos entender o que acontece na Listagem 1: o uso do Generics “” na “List” determina e obriga que apenas objetos do tipo String podem ser adicionados nesta lista, chamada de “listaDeStrings”. Significa que o código da Listagem 2 irá apresentar um erro em tempo de design.
List<String> listaDeStrings = new ArrayList<String>();
listaDeStrings.add(1);
//Erro: The method add(int, String) in the type List<String> is not applicable for the arguments (int)
O erro mostrado ocorre porque não foi encontrado um método com a seguinte assinatura “add(int)” e o compilador tentou usar o método “add(int,string)”, que obviamente não irá aceitar os argumentos passados.
Vamos retornar um pouco para a teoria e depois voltar novamente à prática.
Com a adição do Generics no Java 5, todas as Collections foram reescritas para uso do Generics e consequentemente evitar o famoso “ClassCastException” que ocorria com mais frequência do que queiramos antes de tal recurso existir.
O Generics não é obrigatório, mas é ideal para garantir o type-safety, que em outras palavras irá garantir que determinada collection só tenha o tipo de objetos que você deseja, sem necessidade de casting excessivo. Vejamos um exemplo de como fica o mesmo código com Generic (Listagem 3) e sem Generics (Listagem 4).
import java.util.ArrayList;
import java.util.List;
public class MyGenericApp {
public static void main(String[] args){
//Com Generics
List<Integer> lista = new ArrayList<Integer>();
lista.add(1);
lista.add(2);
lista.add(3);
int total = 0;
for(Integer i : lista){
total += i;
}
System.out.println(total);
}
}
import java.util.ArrayList;
import java.util.List;
public class MyGenericApp {
public static void main(String[] args){
//Sem Generics
List lista = new ArrayList();
lista.add(1);
lista.add(2);
lista.add(3);
int total = 0;
for (int i = 0; i < lista.size(); i++){
total += (int) lista.get(i);
}
System.out.println(total);
}
}
Vamos analisar as diferenças entre as Listagens 3 e 4: na Listagem 3 todo nosso código foi construído com Generics, temos nosso objeto “lista” determinado que só poderá receber objetos do tipo Integer. Sendo assim, podemos ter a certeza que apenas objetos Integer estarão presentes na nossa lista e assim descartamos o uso de type casting desnecessário. Isso é comprovado no nosso foreach, que é feito direto para o tipo Integer sem nenhum casting.
Se você tentar adicionar qualquer objeto que não seja um Integer em tempo de design, automaticamente o compilador já irá apontar o erro e evitar que a aplicação seja iniciada. Por outro lado, na Listagem 4 deixamos e lado o uso do Generics e somos forçados a realizar o casting de cada item para o tipo “int” para só então poder somar na variável “total”.
Agora imagine que a Listagem 4 fosse mudada para o código presente na Listagem 5.
import java.util.ArrayList;
import java.util.List;
public class MyGenericApp {
public static void main(String[] args){
//Sem Generics
List lista = new ArrayList();
lista.add(1);
lista.add(2);
lista.add("3");
int total = 0;
for (int i = 0; i < lista.size(); i++){
total += (int) lista.get(i);
}
System.out.println(total);
}
}
//Erro: Exception in thread "main" java.lang.ClassCastException: java.lang.String
cannot be cast to java.lang.Integer
//at MyGenericApp.main(MyGenericApp.java:17)
Mudamos o terceiro item da nossa lista de inteiro para String e o compilador não acusou nenhum erro, isso porque não temos o Generics para garantir o tipo de dado que queremos. Porém, quando vamos executar a classe e é feita a tentativa de converter String para Integer, acontece o ClassCastException, como mostrado no final da listagem.
Sendo assim, você deve ter notado que o Generics traz robustez, segurança e qualidade (como citado na introdução deste artigo):
- Robustez porque podemos tornar nosso software muito mais abstrato e reflexível, adaptável a várias situações;
- Segurança porque garantimos que nenhum tipo de dado que não desejamos será inserido ou usado me nossa Classe;
- Qualidade porque diminuímos consideravelmente a quantidade de erros que nosso código teria (ClassCastException), menos type casting e mais simplicidade no código.
Mas o Generics não está limitado apenas a List ou ArrayList, por isso, vamos ver mais exemplos de utilização do mesmo. Agora observe a Listagem 6.
import java.util.HashMap;
import java.util.Map;
public class MyGenericApp {
public static void main(String[] args){
Map<String,Object> parametros = new HashMap<String,Object>();
parametros.put("nome", "Velocidade da Luz");
parametros.put("velocidade_da_luz", 300000);
parametros.put("medicao", "km/s");
}
}
Na Listagem 6 nós optamos por usar um Map que com o Generics refere-se respectivamente ao Key e Value do Map. Sendo assim, só poderá adicionar um Key como String e o Value poderá ter qualquer valor, já que é um Object. Este modelo de mapeamento é muito utilizado para parametrização de métodos e processos em Sistemas com grande quantidade de dados, assim podemos identificar qualquer valor de parâmetro apenas sabendo o seu nome.
Um pouco mais prático e complexo
Dada às explicações, teorias e exemplos mostrados nos tópicos acima, iremos agora demonstrar um cenário real onde podemos fazer uso do Generics em vários casos: Collections, Classes, Métodos e etc.
Vamos mapear um cenário real que ocorre com muita frequência, mas caso você não tenha conhecimento suficiente sobre Hibernate /JPA ou Padrões de Projeto, não se preocupe, pois isso não interferirá em nada no seu aprendizado sobre Generics; apenas mostraremos um caso real e para tal precisaremos utilizar tecnologias reais e muito utilizadas no dia a dia.
Cenário: Precisamos criar uma classe DAO (Data Access Object) capaz de buscar, salvar e atualizar qualquer objeto da nossa aplicação, ou seja, esse nosso DAO será a ponte entre o banco e nossa aplicação. Como nosso foco é apenas Generics, nosso DAO terá apenas o método para buscar objetos, assim exemplificamos como utilizar Generics em retorno de métodos e argumentos.
Vamos ser diretos e começar a visualizar nosso DAO (lembrando que estamos simplificando a classe para dar foco apenas no uso do Generics). Observe a Listagem 7.
@Repository(value = "basicDAO")
public class BasicDAO {
@PersistenceContext
protected EntityManager entityManager;
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<MyGlobalBean> findByNamedQuery(String namedQuery,
Map<String, Object> namedParams) {
try {
logger.info("Procurando pela namedQuery " + namedQuery + " com "
+ namedParams.size() + " parametros");
Query query = entityManager.createNamedQuery(namedQuery);
if (namedParams != null) {
Entry<String, Object> mapEntry;
for (Iterator it = namedParams.entrySet().iterator(); it
.hasNext(); query.setParameter(
(String) mapEntry.getKey(), mapEntry.getValue())) {
mapEntry = (Entry<String, Object>) it.next();
logger.info("Param: " + mapEntry.getKey() + ", Value: "
+ mapEntry.getValue());
}
}
List<MyGlobalBean> returnList = query.getResultList();
logger.info("Objetos Encontrados: " + returnList.size());
return returnList;
} catch (Exception e) {
e.printStackTrace();
logger.error("Ocorreu um erro ao executar o findByNamedQuery com parâmetros. MSG ORIGINAL: "
+ e.getMessage());
throw new DAOException(
"Ocorreu um erro ao executar o findByNamedQuery com parâmetros");
}
}
}
O método mostrado acima é um exemplo clássico do uso de Generics. Perceba que nosso método recebe dois parâmetros: um NamedQuery (por agora basta saber que é uma query armazenada no código) e um Map com os parâmetros necessários da query.
Neste ponto já temos o uso do Generics no Map, assim obrigamos a passagem do parâmetro seguindo o formato Key = String e Value = Object e evitamos o risco de ClassCastException.
Esse nosso método retorna um List, mas não é um simples List, é um List de objetos “MyGlobalBean”. A Classe MyGlobalBean é base para todas as outras classes, que são entidades, na nossa aplicação. Por exemplo: Pessoa, Cliente, Funcionario e etc, todas elas estendem do nosso MyGlobalBean. Por esse motivo, a nossa lista de retorno contém uma lista de MyGlobalBean.
Poderíamos também utilizar a seguinte assinatura, presente na Listagem 8, no método acima.
public List<? extends MyGlobalBean> findByNamedQuery(String namedQuery,
Map<String, Object> namedParams)
Qual a diferença de usar o List e o List? A grande diferença é que no primeiro exemplo só podemos adicionar objetos do tipo MyGlobalBean no List, enquanto que no segundo caso podemos adicionar objetos que estendem MyGlobalBean, como os citados acima (Funcionario, Cliente, Pessoa e etc). O uso de um ou outro vai depender da sua lógica de negócio, não existe melhor nem pior.
Aprofundando-nos mais no método “findByNamedQuery” temos a criação de um objeto Query através do createNamedQuery, que no momento não nos interessa entrar em detalhes da sua utilidade.
Apenas para você se situar do que estamos falando, observe a Listagem 9.
Query query = entityManager.createNamedQuery(namedQuery);
Logo abaixo checamos se “namedParams” não é nulo. Caso não seja, significa que temos parâmetros para serem usados na nossa query e teremos quer adicionar todos eles, conforme mostra o código da Listagem 10.
if (namedParams != null) {
Entry<String, Object> mapEntry;
for (Iterator it = namedParams.entrySet().iterator(); it
.hasNext(); query.setParameter(
(String) mapEntry.getKey(), mapEntry.getValue())) {
mapEntry = (Entry<String, Object>) it.next();
logger.info("Param: " + mapEntry.getKey() + ", Value: "
+ mapEntry.getValue());
}
}
Imagine o código acima sem uso de Generics: ia ser um caos de type-casting para todo lado, mas não temos que nos preocupar com isso, pois usando o “Entry” vamos percorrer todos os parâmetros com a certeza que sua chave(key) sempre será uma String e seu valor um Object, pois a query pode ter parâmetros do tipo int, String, double e etc. Como não sabemos qual o tipo do parâmetro que a query exige, então usamos o object, a única certeza que temos é que o label/nomeação do parâmetro será uma String.
Por fim, nós temos o retorno da lista com o tipo necessário, conforme mostra o código da Listagem 11.
List<MyGlobalBean> returnList = query.getResultList();
Imagine que o DAO seja a camada C e os formulários e classes de comunicação direta com o usuário fazem parte da camada A. Está sentindo falta da camada B? Esta é uma camada intermediária que guarda a maior parte da lógica de negócio e faz a ponte entre a interface do usuário e o core do sistema. Explicamos isso apenas para que vocês possam entender que não é foco deste artigo criar a camada B, foge totalmente do nosso escopo, mas é bom que se entenda que ela deve ser criada em um caso real para proporcionar maior qualidade e flexibilidade ao software.
Criaremos então um ManagedBean (que faz parte da camada de visualização – A) para chamar nosso método findByNamedQuery dentro do DAO. Se você não tem conhecimento de JSF, não se preocupe, por agora basta saber que o ManagedBean é uma classe responsável por permitir a interação direta do cliente (XHTML com JAVA), ou seja, o cliente pode explicitamente chamar um método do ManagedBean através do seu XHTML. Fazendo analogia com o JSE, podemos comparar com um formulário em Swing que permite interação do usuário.
Sendo assim, vejamos nosso ManagedBean simples no código da Listagem 12.
@ManagedBean(name = "clienteMB")
@ViewScoped
public class MyManagedBean {
public List<Cliente> findAllBeans() {
Map<String,Object> params = new HashMap<String,Object>();
params.put(“nome”,”Devmedia”);
return (List<Cliente>) myDAO.findByNamedQuery(Cliente.FIND_ALL_COMPLETO,params);
}
}
Criamos a classe MyManagedBean com o método “findAllBeans” que retorna uma Lista de objetos do tipo Cliente (tenha em mente que Cliente estende de MyGlobalBean, como explicamos anteriormente).
Criamos um parâmetro com o label “nome” e o valor “Devmedia” e passamos para o método findByNamedQuery que irá inserir esse parâmetro corretamente na nossa query. Em “params.put()” você deverá passar sempre um String seguido de um valor qualquer. Perceba que qualquer tentativa que fuja desse padrão é “barrada” automaticamente em tempo de design, isso é o Generics.
Com isso, concluímos o nosso artigo que teve um foco o mais didático possível para mostrar os princípios que regem o recurso conhecido como GENERICS. Além disso, mostramos exemplos reais do uso destes, dando maior importância no retorno de métodos e passagem de parâmetros com Generics.