Olá Pessoal, tudo bom?
No post de hoje iremos falar sobre um tema que muitos desenvolvedores utilizam, mas poucos compreendem. Vamos falar sobre Generics ou aqueles sinais malucos de maior e menor que são utilizados ao declarar uma instância de List 😀
Essa é mais uma importante adição realizada pela versão 1.5 do Java e que é muito utilizada, mas é um dos temas mais complicados de compreensão que existem na linguagem Java, dessa forma esse post visa detalhar um pouco melhor Generics mas de uma forma um pouco mais amigável principalmente para iniciantes no desenvolvimento Java.
Grande parte deste post foi elaborado utilizado o livro Java Generics and Collections ((Java Generics and Collections – http://shop.oreilly.com/product/9780596527754.do)), assim fica a dica para quem quiser se aprofundar mais no tema.
O que é Generics?
Generic é um nova extensão adicionada na linguagem Java a partir da versão 1.5 a qual provê em tempo de compilação uma verificação de type-safety de código, removendo riscos de ClassCastException durante a execução, o qual era um erro comum antes da versão 1.5; Essa verificação consiste em verificar se o que está sendo atribuído a uma instância de uma classe está de acordo com o especificado. Por exemplo, veja o código da Listagem 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/////////////////////////////////////////////////// // Antes do Java 1.5 /////////////////////////////////////////////////// List palavras = new ArrayList(); palavras.add("ABC"); //Tudo ok, compatível com o especificado palavras.add(12); //Erro de ClassCastException em tempo de execução /////////////////////////////////////////////////// // Após o Java 1.5 /////////////////////////////////////////////////// List<String> palavras = new ArrayList<String>(); palavras.add("ABC"); //Tudo ok, compatível com o especificado palavras.add(12); //Erro de compilação |
Repare que os erros que ocorrem na linha de adição do integer 12. Antes do Java 1.5, o erro ocorria somente em execução, o que dependendo do caso poderia ser pego somente quando o software estivesse em produção. Já após o Java 1.5, a linha que adiciona o integer 12 daria um erro de compilação, devendo ser corrigido tem tempo de desenvolvimento do software.
Ao utilizar Generics o javac age como um corretor ortográfico. Ao escrever um código no qual existem erros na tipagem, o javac irá mostrar esses erros e pedir para você realizar a correção. Dependendo do erro, ele pode mostrar uma possível correção, mas nem sempre isso pode ocorrer.
Generics chegou atrasado para ser utilizado com o framwork da Collections, que permitiu uma abstração excelente sobre estruturas de dados. Apesar de parecer muito complexa devido aos sinais de menor-maior ( < , > ) e pelos seus “wildcards”, a partir do momento que você compreende o porque da utilização e como realizar esta utilização, você deverá se sentir muito confortável para utilizar Generics em seus códigos.
Se você for um iniciante que não está tão familiarizado com Generics é uma excelente hora de preencher essa lacuna existe em sua carreira 🙂 . Não apenas irá ajudar a evitar umas ClassCastException em execução como irá dar mais confiança sobre o comportamento de seu código.
Um último detalhe é que Generics podem ser aplicadas em classes, métodos e atributos, além de interfaces e enums. Assim você pode utilizar as notações para atender necessidades que forem sendo criadas em seus projetos.
Como Generics funcionam?
Vamos relembrar como funciona como é o processo de compilação de uma classe Java de acordo com a Figura 01
Após verificar em tempo de compilação se a sintaxe do código está de acordo com as específicações de Generics e Java, o compilador irá gerar o código em Java byte-codes, mas sem a sintaxe generics, convertendo isso para o que é chamado de raw type. Todo o tipo de informação relacionada ao generics é removida durante essa fase, o nome desta operação é chamada Type Erasure.
Assim lembrando do código da Listagem 1 acima, o código voltaria ao que era na versão antes da 1.5. Esse processo é realizado por detrás dos panos, não sendo necessária a preocupação se está sendo realizado da maneira correta, pois em tempo de compilação o javac já garantiu que o código é type-safe. Dessa forma é fácil perceber que Generics nada mais é que um mecanismo para verificar se o que você está desenvolvendo está nos trilhos corretos. Se tudo estiver ok, não existirá erros de compilação. Senão existem erros é possível executar esse código. Se é possível executar é possível acreditar que não ocorrerão erros de type cast.
Exemplos Simples
Vamos começar retratando alguns exemplos simples de Generics
1) Existem muitas interfaces e classes que são parametrizadas, como por exemplo a classe java.util.Set ((API java.util.Set – http://docs.oracle.com/javase/7/docs/api/java/util/Set.html)), dessa forma é possível criar classes “dinâmicas” de tipos como na Listagem 2:
1 2 |
Set sets = new HashSet<String>(); sets = new HashSet<Integer>(); |
Lógico que a instância do HashSet não é dinâmica, apenas o ponteiro desta pode apontar para Hashs de vários tipos, por não limitarmos o ponteiro para um determinado tipo.
2) Ao utilizar um generic com o tipo como Object, isso deixará a classe parametrizada para qualquer tipo de objeto, ou seja, podemos inserir Strings, Integers, Aviões, Ambientes e o que mais for necessário neste tipo de classe. Mas independente do tipo, um set de uma classe dessas será sempre do tipo Object, podendo ocorrer ClassCastExceptions, por causa dos casts necessários após o set.
1 2 3 4 5 6 |
List<Object> listQualquerTipo = new ArrayList<Object>(); listQualquerTipo.add("abc"); //Sem problemas de compilação listQualquerTipo.add(new Float(3.0f)); //Sem problemas de compilação for(int i = 0; i < list.size(); i++){ String s = (String) list.get(i); //Ocorrerá erro de execução na segunda iteração } |
3) Um classe que é parametrizada com o ? (Interrogação) representa uma classe de parâmetros de um tipo desconhecido. Dessa forma você pode atribuir a ela qualquer tipo, como String ou Integer.
1 2 |
List<?> listaTipoDesconhecido = new ArrayList<String>(); listaTipoDesconhecido = new ArrayList<Integer>(); |
4) Ao trabalhar com classes parametrizadas que possuem um hierarquia de herança, é possível instanciar classes filhas para que um ponteiro da classe pai receba essas instâncias.
1 2 |
Set<String> setString = new HashSet<String>(); //Valido setString = new LinkedHashSet<String>(); //Valido |
Mas é uma herança entre as classes, pois o parametro do generic não suporta diretamente
1 |
Set<Object> SetOfObject = new HashSet<String>(); //Erro de compilação - Tipos incompatíveis |
5) Mas existem formas do parâmetro do Generics aceitar herança, basta utilizar a palavra chave extends e a classe pai requerida, por exemplo. Isso é chamado Bounded Type Parameters:
1 2 3 |
Set<? extends Number> setDeNumbers = new HashSet<Integer>(); //OK - Integer extends Number setDeNumbers = new HashSet<Float>(); //OK - Float extends Number setDeNumbers = new HashSet<String>();//Erro - String não extends Number |
6) Da mesma forma é possível utilizar a palavra chave super, para utilizar uma herança ao inverso, ou ainda as interfaces que esta implementa e ainda observando a classe em si, por exemplo:
1 2 3 |
Set<? super TreeMap> setOfAllSuperTypeOfTreeMap = new LinkedHashSet<TreeMap>(); //OK, TreeMap é super dela mesma setOfAllSuperTypeOfTreeMap = new HashSet<AbstractMap>(); //OK AbstractMap é pai da classe TreeMap setOfAllSuperTypeOfTreeMap = new LinkedHashSet<Map>(); //OK Map é uma interface implementada por TreeMap |
7) Não é possível utilizar Generics nas classes literais (.class), sendo o único local que não é permitido utilizar o generics e sim somente o raw type.
1 2 |
List.class //OK List<String>.class //Erro |
8) Ao desenvolver métodos com Generics, é necessário declarar os tipos dos parâmetros na assinatura do método, entre os modificadores e o tipo do retorno, por exemplo:
1 2 3 |
public static <T> T igual(T origem){ return origem; } |
Esse parâmetro T deverá ser criado na declaração da classe, pois senão ocorrerá erro de compilação.
Notações e Convenção de Nomes
Uma das razões de Generics não parecer ser simples, está relacionado a quantidade de termos e convenções para nomes que existem. A partir do momento em que estes itens se tornam mais familiar, com o conhecimento do para que serve cada um, torna-se mais fácil a interpretação destes símbolos. A seguir as notações mais comuns ao utilizar Generics:
Notação | Significado |
---|---|
<E> | Tipo Genérico; É chamado de um parâmetro formal; Utilizado para denotar um Elemento |
<T> | Tipo Genérico; É chamado de um parâmetro formal; Utilizado para denotar um Tipo |
<Integer> | Tipo Parametrizado como uma classe Integer |
<?> | Representa uma classe desconhecida |
Set | Raw Type, ou seja, uma classe desprovida de notações generics |
<? extends T> | Representa o tipo parametrizado como uma classe que seja da instância de T ou filha de T |
<T extends Comparable> | Representa que o tipo parametrizado T seja uma classe que implemente a interface Comparable |
<T extends Aviao> | Representa que o tipo parametrizado T seja uma classe que implemente a classe Aviao |
<? Super T> | Representa o tipo parametrizado T que seja uma instância de T ou pai de T, seja classe ou interface |
<T super Comparable> | Representa que o tipo parametrizado T implemente a interface Comparable ou um de seus pais |
<T extends Comparable<T>> | Tipo Parametrizado Recursivo, T deve ser do tipo de uma classe que realize uma implementação da interface Comparable para comparações com a classe T |
public abstract class BilheteBC<E extends Bilhete> extends PatternBC<E> | Declaração de uma classe que recebe um tipo parametrizado E, o qual estende a classe Bilhete. Além disso a classe BilheteBC estende a classe PatternBC a qual é obrigada a receber instâncias de E |
Benefícios e Vantagens
A introdução de Generics dentro da Linguagem Java trouxe muitos benefícios para o desenvolvimento, como por exemplo:
Type-Safety
Sem dúvida é a principal vantagem do Generics. O framework Collections, antes da JDK 1.5 era desprotegido dessa segurança, pois acabava tendo que aceitar qualquer tipo de Object, independente se a collection poderia conter apenas Strings, por exemplo. A partir da introdução do Generics, criou-se o conceito de type-safe collection, pois ao criar uma collection de Strings essa automaticamente estará protegida contra outros tipos e classes, evitando assim muitas ClassCastExceptions já em tempo de compilação.
1 2 3 4 5 |
List<String> palavras = new ArrayList<String>(); palavras.add(“coins”); //OK, sem erros palavras.add(new Aviao()); //Erro! A list não aceita avioes, somente Strings palavras.add(42); //Erro! A list não aceita o sentido da vida e do universo! |
Abaixo aos Casts!
Com Generics não é necessário realizar type casts, pois ele o faz automaticamente para você! Veja abaixo como era na versão pré 1.5 e agora a partir da versão 1.5, repare que deixa o código mais claro e robusto!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/////////////////////////////////////////////////// // Antes do Java 1.5 /////////////////////////////////////////////////// List itens = new ArrayList(); itens.add("chocolates"); String item = (String) itens.get(0) /////////////////////////////////////////////////// // Após o Java 1.5 /////////////////////////////////////////////////// List<String> itens = new ArrayList(); itens.add("chocolates"); String item = itens.get(0) //Cade o cast que estava aqui??? Tá no Generics! |
Extinção do ClassCastException!
Com o compilador javac garantindo que as classes receberão os tipos corretos no momento da compilação do código Java, a exception ClassCastException praticamente desapareceu dos programas Java! Isso é excelente!
Pontos Importantes
Generics não podem ser aplicadas sobre tipos primitivos (int, long, char, boolean). Mesmo com a existência das classes Wrappers (Integer, Long, Character, Boolean) e o auto-boxing (conversão automática entre tipo primitivo e wrapper – int e Integer, por exemplo) não é possível utilizar tipos primitivos com Generics. Meio estranho mas é a vida!
1 Holder<int> numbers = new Holder<int>(10); //Erro de compilação! unexpected type required: reference found:int
Uma classe, interface parametrizada com Generics usa tipos formais para recuperar a informação do tipo ao criar uma nova instância dessa classe. Por exemplo o código da seguinte interface:
1234 public interface Cache <K,V>{public V get();public V put(K key, V value);}Ao criar uma classe que implemente uma classe parametrizada, essa interface por exemplo, é necessário passar as classes que serão representadas por esses valores, por exemplo Integer e String:
1234567891011121314 public class TesteCache implements Cache<Integer, String>{@Overridepublic String get() {//Escreva algo aquireturn null;}@Overridepublic String put(Integer key, String value) {//Escreva algo aquireturn null;}}
Generics são relacionadas com Templates em C++, mas diferente deles, que criam novos tipos para cada parâmetro específico, em java as classes parametrizadas são compiladas apenas uma vez e mais importante é que é gerado apenas um único arquivo, visto que Generics é utilizado na fase de compilação e na execução o javac remove todas as notações referentes a Generics.
Type Inference: A partir da versão 7 o Java foi acrescido de um novo operador, o diamont (<>). A utilidade deste operador está na redução do código redundante durante a instanciação de uma classe parametrizada. Assim o Generic declarado na variável que irá armazenar a classe é entendido pelo javac como o mesmo na chamada ao construtor.
12345678 ///////////////////////////////////////////////////// Antes do Java 7///////////////////////////////////////////////////Map<String, Set<Integer>> contacts = new HashMap<String, Set<Integer>>();///////////////////////////////////////////////////// Após o Java 7///////////////////////////////////////////////////Map<String, Set<Integer>> contacts = new HashMap<>();
finally{
Generics é um assunto extremamente vasto em Java. O Objetivo deste post foi explicar os itens mais básicos, iniciando a pavimentação de um longo que caminho que exploraremos ao longo dos futuros posts deste site.
Duvidas ou sugestões? Deixe seu feedback! Isso ajuda a saber a sua opinião sobre os artigos e melhorá-los para o futuro! Isso é muito importante!
Até um próximo post!
Douglas Oliveira says
Muito bom, Parabens,
Mas algumas coisas me deixaram com duvidas em relação a interrogação (?), se ela serve para se referenciar a qualquer tipo, o T não supriria essa necessidade ?
Exemplo
List lista; seria a mesma coisa que List ?
Mauda says
Olá Douglas, tudo bom?
Obrigado! e Desculpe a demora em responder.
O ponto de interrogação (?) realiza o mesmo trabalho do T, mas diferente do T você não pode setá-lo em uma declaração de atributo, por exemplo:
public void compare(List< ?> lista){
for(? elemento : lista){ //ERRO DE COMPILACAO
//realiza uma operacao de comparacao
}
}
Se tiver mais dúvidas por favor faça um novo comentário!
Obrigado.
Mauda