_generics
Coisas que você
não sabia sobre
Generics
Veja neste artigo como tirar todo
proveito da utilização de tipos
genéricos na linguagem Java.
Eduardo Guerra | [email protected]
é desenvolvedor de frameworks, editor-chefe da revista MundoJ e pesquisador do Instituto Nacional de Pesquisas Espaciais (INPE).
Foi professor do Instituto Tecnológico de Aeronáutica (ITA), onde concluiu sua graduação, mestrado e doutorado. Suas pesquisas se
concentram nas áreas de design, arquitetura e teste de software. Possui diversas certificações da plataforma Java e experiência como
arquiteto de software. Participa de projetos de frameworks open-source, como SwingBean, Esfinge e ClassMock. É autor do livro
“Design Patterns com Java: projeto orientado a objetos guiado por padrões” lançado pela editora Casa do Código. Ele acredita que
um bom software se faz mais com criatividade do que com código e vive em busca de melhores formas para seu desenvolvimento. O
suporte a tipos genéricos foi adicionado na linguagem Java a partir do JDK 1.5. A princípio ela
foi anunciada como uma funcionalidade “opcional“,
que poderia ou não ser utilizada pelos desenvolvedores. É verdade que todo código legado continuou
compilando sem o uso de tipos genéricos, porém na
minha opinião qualquer funcionalidade adicionada
como parte da linguagem está longe de ser opcional para os desenvolvedores. Acredito que na época
a ideia era não causar alarde, porém, hoje, o conhecimento de tipos genéricos é essencial a qualquer
programador Java.
O uso de tipos genéricos é muito conhecido pela
sua utilização na API de coleções, que disponibiliza
um conjunto de classes cujo uso é essencial a qualquer desenvolvedor. Quando se cria uma coleção,
seja ela uma lista (List) ou um conjunto (Set), normalmente se cria a coleção para armazenar alguma
coisa, ou seja, algum tipo específico de classe. É aí
que entram os tipos genéricos, para dizer se aquela
lista é uma lista de pessoas (List<Pessoa>) ou se o
conjunto é um conjunto de produtos (Set<Produto>).
Indo na contramão de algumas linguagens dinâmicas, onde a ideia é ter o menor número de definições possível para tornar o código mais simples
de ser criado, os tipos genéricos adicionam certa
verbosidade na linguagem em troca de uma maior
segurança de código. Por exemplo, antes dos tipos
genéricos, qualquer objeto poderia ser adicionado
em uma lista, e isso criava a possibilidade que se tentasse realizar o cast desse objeto para a classe errada,
causando erros em tempo de execução. Com o uso
de tipos genéricos, pelo fato do tipo da coleção ser
explicitamente definido, esse tipo de erro na maioria
dos casos é detectado em tempo de compilação.
A ideia deste artigo é mostrar algumas coisas sobre os tipos genéricos que a maioria das pessoas não
sabe. Você sabia, por exemplo, que em alguns casos
você consegue recuperar o tipo genérico utilizando
reflexão? Você sabia que usando tipos genéricos você
pode definir o tipo da exceção que será lançada por
um método através da classe que o invoca? Consegui
despertar sua curiosidade? Então vamos começar o
artigo com aquela revisada de tipos genéricos para
aqueles que estão um pouco enferrujados e em seguida vamos direto para a parte mais divertida.
Tipos Genéricos em Java
A melhor forma de entender um tipo genérico é
como um parâmetro para um tipo. Muitas vezes você
quer definir uma classe, mas quer que o retorno de
certos métodos ou que certos parâmetros possam ser
5\
O suporte a tipos genéricos é uma funcionalidade de linguagem
que muitas vezes é subutilizada pelos desenvolvedores. Eu percebo que muitos acabam se limitando a utilizar classes com tipos genéricos, mas não consideram inclui-los em suas próprias
classes. Este artigo mergulha nas questões mais avançadas e
curiosas dos tipos genéricos, mostrando como eles podem ser
utilizados de uma forma que você nunca tinha imaginado.
definidos por quem está instanciando um objeto daquela classe. A lista é um ótimo exemplo, pois quem
a instancia para armazenar um determinado tipo vai
querer que o método de adição só aceite aquele tipo
e que o método de recuperação retorne aquele tipo.
A Listagem 1 mostra um exemplo de como uma
classe define um tipo genérico e como essa classe é
instanciada. Repare que na definição da classe Grafo, o tipo genérico <E> é definido em sua declaração
e utilizado na definição dos métodos, no lugar de
parâmetros e/ou retornos. Dessa forma, quando um
objeto instancia a classe e define um tipo genérico, é
como se para aquela instância todos os lugares onde
foi definido E fossem substituídos pelo tipo configurado.
Listagem 1. Exemplo de uma classe que define um
tipo genérico.
//Definição de classe com tipo genérico
public class Grafo<E> {
public void adicionarNo(E no){ ... }
public void adicionarLigacao(E noA, E noB) { ... }
public List<E> recuperaVizinhos(E no) { ... }
}
//Instanciando a classe definindo tipo genérico
Grafo<Cidade> grafo = new Grafo<Cidade>();
Outro caso de utilização de tipos genéricos é
quando se deseja definir uma interface e quer que a
classe que vai implementá-la possa definir o tipo de
alguns retornos e parâmetros. Dessa forma, podese definir uma interface de propósito mais geral e
inserir parâmetros de acordo com a classe que está
a implementando. De forma alternativa, a classe
pode “propagar“ o tipo genérico para sua definição,
deixando assim para que ele seja definido no
momento de instanciar os objetos, como mostrado
na Listagem 1.
/6
A Listagem 2 apresenta o exemplo da interface DAO<E> que define um tipo genérico e da classe
ProdutoDAO que implementa essa interface fixando
o tipo genérico que será utilizado. Dessa forma, na
própria classe, os retornos e parâmetros definidos
utilizando o tipo genérico na interface, serão fixados
de acordo com o tipo definido. Dessa forma, quem
instanciar a classe ProdutoDAO nem saberá que ela
implementa uma interface com um tipo genérico.
Listagem 2. Exemplo de uma interface que define
um tipo genérico.
//Definição de interface com tipo genérico
public interface DAO<E> {
public void salvar(E obj);
public E recuperar(int id);
public List<E> listarTodos();
}
//Definindo uma classe que fixa o tipo genérico
public class ProdutoDAO implements DAO<Produto> {
public void salvar(Produto obj) { ... }
public Produto recuperar(int id) { ... }
public List<Produto> listarTodos(){ ... }
}
Além de poder ser definido em classes em
interfaces, o tipo genérico também pode ser
definido no contexto de um método. O principal
objetivo desse uso é amarrar os tipos dos
parâmetros passados ou permitir que o retorno seja
inferido de acordo com o parâmetro passado.
A Listagem 3 apresenta o exemplo de um método que define um tipo genérico para amarrar os
parâmetros. Observe que ele recebe como parâmetro
uma List<E> e um E, então a lista precisa ser do mesmo tipo do elemento, senão será apontado um erro
em tempo de compilação. Por exemplo, um código
que passar um List<Integer> e uma String como parâmetro não será válido.
Listagem 3. Exemplo de método que usa tipos genéricos para amarrar os parâmetros.
public static <E> void colocarNaFrente(List<E> lista,
E elemento) { ... }
A Listagem 4 apresenta outro exemplo, onde o
tipo genérico é utilizado para que o tipo do retorno
seja inferido de acordo com o parâmetro passado. Por
exemplo, se for passada uma lista de String, então o
retorno será do tipo String. Isso evita que casts desnecessários sejam feitos e evita erros como a atribuição do retorno para uma variável de tipo incompatível com o objeto retornado.
Listagem 4. Exemplo de método que usa tipos genéricos para inferência de retorno.
//Definição do método
public static <E> E elementoDoMeio(List<E> lista) { ... }
//Uso do método
List<String> lista = //recupera lista
String meio = elementoDoMeio(lista)
ou interface é uma escolha válida.
Outro fato importante no exemplo da Listagem
5 é que, nesse caso, como o parâmetro não tem um
tipo genérico definido, nenhum método que recebe
o tipo genérico como parâmetro pode ser invocado,
a não ser passando “null“. De forma mais concreta, o
método add(), que recebe o tipo genérico da lista, não
poderia ser invocado nesse caso. Sendo assim, seria
como se a lista fosse read-only.
Outro caso em que isso pode ser muito útil é para
restringir o tipo de classe que pode ser configurada
em uma anotação. Por mais que possa parecer estranho, a classe Class possui um tipo genérico que é o
tipo da própria classe que está representando. Sendo
assim, a classe String retorna no método getClass()
uma instância de Class<String>. Sendo assim, para
tanto um método ou uma anotação que recebe uma
classe como parâmetro, é possível definir apenas um
subconjunto de classes que podem ser passadas como
parâmetro.
Imagine, por exemplo, uma anotação chamada
@RelatedDAO que configura qual a classe DAO que
precisa ser utilizada para uma determinada entidade. Devido a própria descrição, só faz sentido a configuração de classes que implementem uma interface
chamada DAO. A Listagem 6 mostra como seria essa
anotação.
De forma bem rápida e resumida essa pequena introdução mostrou um pouco do funcionamento dos
tipos genéricos. As próximas seções irão focar em
coisas interessantes que podem ser feitas com tipos
genéricos.
Listagem 6. Anotação que restringe a Class configurada em uma propriedade.
Restringindo Tipos Genéricos
@Retention(RetentionPolicy.RUNTIME)
Uma coisa que poucos sabem sobre os tipos gené@Target(ElementType.TYPE)
ricos é que é possível restringir quais tipos podem ser public @interface RelatedDAO {
passados como parâmetro. Para ilustrar essa questão
Class<? extends DAO> value();
considere um método que grave em um arquivo uma }
lista de objetos. Como os objetos precisarão ser serializados, não faz sentido receber uma lista em que os Para finalizar essa parte de restrição de tipos, uma
objetos não implementem Serializable. A Listagem 5 classe também pode restringir os tipos genéricos que
podem ser utilizados para parametrizá-la. Imagine
mostra como esse método poderia ser definido.
que ao invés de criar um método para guardar uma
Listagem 5. Exemplo de método que restringe o tipo lista em um arquivo, eu decida criar uma lista serialigenérico do parâmetro.
zável. Observe na Listagem 7 que é possível, mesmo
implementando outra interface, restringir na declapublic void gravarEmArquivo(List<? Extends
Serializable> lista) { ... }
ração do tipo genérico qual classe ou interface ele
precisa ter como supertipo. Nesse caso, ao invés do
Quando a restrição é utilizada em parâmetros de mé- wildcard “?“, utiliza-se o próprio parâmetro genérico.
todos, o “?“, chamado de wildcard, pode ser utilizado
para definir um tipo que obedece certa regra de tipo. Listagem 7. Exemplo de método que usa tipos genéSe o wildcard for acompanhado de “extends“ significa ricos para inferência de retorno.
que ele pode ser qualquer classe que estenda a classe public class SerializableList<E extends Serializable>
ou implemente a interface. Se ele for acompanhado implements List<E> { ... }
de “super“ significa que pode ser qualquer supertipo
do configurado. Em ambos os casos a própria classe Uma das vantagens desse tipo de restrição é que se
7\
você define que um tipo genérico deve implementar
uma interface, então é possível invocar os métodos
daquela interface mesmo que o tipo não esteja definido. Esse tipo de definição também é útil para evitar
que erros de tipagem ocorram na definição dos tipos
genéricos.
Ok! Eu sei que essa seção não impressionou tanto
quem conhecia um pouco mais sobre tipos genéricos!
Mas prepare-se que na próxima seção as coisas ficam
mais interessantes!
Definindo o Tipo da Exceção pelo
Cliente
Qualquer desenvolvedor Java aprende ao lidar
com exceções que um método pode declarar as exceções que podem ser lançadas durante a sua execução.
Mas poderia um cliente definir o tipo da exceção que
será lançada por um método que ele invoca? Surpreendentemente utilizando tipos genéricos sim!
Muitas vezes acabamos capturando a exceção
lançada por um componente para a encapsularmos
dentro de uma exceção definida pela aplicação. Nesse
cenário seria muito interessante que fosse possível
definir para esse componente qual exceção ele deve
lançar, pois dessa forma não seria preciso mapear as
suas exceções para as da aplicação.
O exemplo dessa seção considera uma classe que
autentica usuários a partir de definições em um arquivo de propriedades com login e senha. Um dos requisitos é que esse componente lance uma exceção
toda vez que um usuário não for autenticado. Para
permitir seu reúso, deseja-se então que o cliente possa definir a classe da exceção que será lançada.
O segredo dessa solução está na definição de uma
interface com tipo genérico para representar uma fábrica de exceções, como a classe FabricaExcecoes na
Listagem 8. O tipo genérico dessa interface representa a exceção que é criada por ela. A intenção é que as
classes ao implementarem essa interface definam o
tipo da exceção que irão criar.
Listagem 8. Interface com tipo genérica para criação
de exceções.
public interface FabricaExcecoes<E extends Exception> {
public E criarExcecao(String msg);
}
A Listagem 9 apresenta a classe Autenticador, a
qual é responsável pela autenticação dos usuários.
No construtor é feito o carregamento do arquivo de
propriedades “users.prop” onde os usuários e suas
senhas estão armazenadas (a ideia aqui é apenas
ilustrar a técnica, não armazene senhas assim “em
casa“). O método autenticar() define um tipo genérico E, que é utilizado para um parâmetro do tipo
FabricaExcecoes<E> e para o tipo de exceção lançada
/8
Cuidado! Os tipos genéricos são invariantes!
Uma coisa que confunde muitas pessoas que começam a trabalhar com tipos genéricos é como
funciona a herança e o polimorfismo. Os arrays
são tipos covariantes, isso significa que um array de um tipo A é considerado por polimorfismo
um array do tipo B, se B for um supertipo de A.
Por exemplo, se eu tenho uma variável do tipo
Integer[] então ela pode ser atribuída para uma
do tipo Number[] via polimorfismo.
Com os tipos genéricos as regras são diferentes,
pois eles são invariantes. Nesse caso, uma classe
nunca pode ser convertida por polimorfismo para
uma classe com um tipo diferente. Isso significa
que uma variável do tipo List<Integer> não pode
ser atribuída para uma do tipo List<Number>. Se
você quiser receber variáveis com mais de um
tipo genérico, é preciso utilizar o wildcard com
as palavras super e extends de acordo com a restrição desejada.
No caso de métodos que utilizarem um wildcard
com extends em um parâmetro, existe a restrição de não ser possível invocar nessa instância
métodos que recebem o tipo genérico. De forma
equivalente, quando o wildcard com super for
utilizado, não pode ser feita a inferência do tipo
do retorno, que deverá ser sempre recebido como
Object.
por ele. Dessa forma, o tipo da exceção lançada irá depender do tipo genérico da fábrica, a qual é definida
pelo cliente. Observe que a exceção lançada é criada
pela fábrica.
Listagem 9. Classe que faz a autenticação de um
usuário e declara uma exceção genérica.
public class Autenticador {
private Properties p;
public Autenticador(){
try {
p = new Properties();
p.load(new FileInputStream(“users.prop”));
} catch (Exception e) {
throw new RuntimeException(“Usuarios não
encontrados”);
}
}
public <E extends Exception> void
autenticar(String login, String senha,
FabricaExcecoes<E> f) throws E {
}
}
if(!(p.contains(login) &&
p.get(login).equals(senha))){
throw f.criarExcecao(“Login e senha incorretos!”);
}
}
Para entender melhor como isso funciona, será
mostrado um exemplo concreto. A Listagem 10 apresenta a exceção SecurityException, que no exemplo
faz o papel de uma exceção que foi definida para a
aplicação. Já na Listagem 11, a classe FabricaSecurityException implementa a interface FabricaExcecoes
com o tipo genérico igual a exceção definida, no caso
SecurityException.
Listagem 10. Exceção definida para ser lançada pelo
método.
public class SecurityException extends Exception {
}
public SecurityException(String message) {
super(message);
}
Listagem 11. Implementação da fábrica de exceções
para a exceção definida.
public class FabricaSecurityException implements
FabricaExcecoes<SecurityException> {
}
@Override
public SecurityException criarExcecao(String msg) {
return new SecurityException(msg);
}
A Listagem 12 apresenta um código cliente que utiliza a classe Autenticador. Observe que uma instância
da classe FabricaSecurityException é criada para que
o tipo da exceção possa ser definido. Quando essa
classe é passada como parâmetro, o tipo da exceção
é inferido pelo tipo genérico da fábrica. Sendo assim,
observe que o bloco try/catch captura a exceção que é
definida pela aplicação.
Listagem 12. Código que invoca a classe Autenticador definindo a exceção que ela irá lançar.
public class Principal {
public static void main(String[] args) {
Autenticador a = new Autenticador();
FabricaSecurityException f =
new FabricaSecurityException();
try {
a.autenticar(“admin”, “admin”, f);
}
System.out.println(“Autenticação com sucesso”);
} catch (SecurityException e) {
System.out.println(e.getMessage());
}
Outra abordagem nessa solução seria a utilização de
um tipo genérico de classe ao invés de um tipo genérico de método. Dessa forma, a fábrica de exceções
precisa ser configurada apenas uma vez no construtor
e não precisa ser passada todas as vezes como parâmetro. Da outra forma, a mesma instância poderia
lançar diferentes exceções de acordo com o parâmetro do método, e nessa solução a exceção é definida
no momento da criação. A Listagem 13 mostra como
ficaria essa implementação.
Listagem 13. Utilizando tipo genérico de classe no
Autenticador.
public class Autenticador<E extends Exception> {
private Properties p;
private FabricaExcecoes<E> f;
public Autenticador(FabricaExcecoes<E> f){
this.f = f;
try {
p = new Properties();
p.load(new FileInputStream(“users.prop”));
} catch (Exception e) {
throw new RuntimeException(“Usuarios não
encontrados”);
}
}
public void autenticar(String login, String senha) throws
E{
if(!(p.contains(login) &&
p.get(login).equals(senha))){
throw f.criarExcecao(“Login e senha incorretos!”);
}
}
}
Recuperando o Tipo Genérico com
Reflexão
Uma coisa que sempre é comentada sobre os tipos
genéricos, é que é são validações realizadas em tempo
de compilação e que essa informação não é mantida
em tempo de execução. Sendo assim, imagino que talvez você esteja se perguntando: como um tipo genérico
pode ser recuperado por reflexão se ele não é mantido
pela máquina virtual? Calma! Dado um objeto, não é
possível saber o tipo genérico desse objeto em tempo
de execução. Porém, em declarações de métodos, atributos ou de classe que utilizam tipos genéricos, é sim
possível saber qual o tipo genérico declarado.
9\
Considere, por exemplo, a classe Pessoa apresentada na Listagem 14. Imagine que fosse necessário
criar um algoritmo que recuperasse todas as entidades relacionadas com uma determinada classe. Nesse
contexto, uma entidade seria toda classe com a anotação @Entity. Nesse caso, seria necessário varrer
os atributos dessa classe e procurar pelos tipos que
possuem essa anotação. Porém, por exemplo, o atributo telefones possui uma entidade declarada como
um tipo genérico. Como fazer para buscar essa informação?
ParameterizedType tipoGenerico =
(ParameterizedType) f.getGenericType();
Class parametroGenerico = (Class)
tipoGenerico.getActualTypeArguments()[0];
if(parametroGenerico.isAnnotationPresent(
Entity.class)){
list.add(parametroGenerico);
}
}
}
return list;
}
Listagem 14. Classe que possui tipos relacionados
}
declarados em tipos genéricos.
@Entity
public class Pessoa {
private
private
private
private
private
String nome;
int idade;
Endereco endereco;
List<Telefone> telefones;
Set<String> cargos;
A Listagem 16 apresenta um exemplo de como esse
método seria utilizado para retornar as entidades relacionadas na classe Pessoa e imprimir no console.
No caso, será impresso o nome das classes Telefone e
Endereco que possuem a anotação.
Listagem 16. Classe que possui tipos relacionados
declarados em tipos genéricos.
public class PrincipalReflection {
//getters e setters omitidos
}
Na API de reflexão, a classe Field possui um método chamado getGenericType(), que não retorna os
parâmetros de tipo, mas uma instância de ParameterizedType (apesar do retorno ser do tipo Type e ser
necessário fazer o cast). Essa classe possui um método chamado getActualTypeArguments() que retorna
um array com os parâmetros genéricos configurados.
A Listagem 15 mostra o método getEntidadesRelacionadas() que recebe uma classe e retorna uma lista com as entidades relacionadas a ela. Primeiramente esse método verifica se a classe do atributo possui
a anotação @Entity e, em caso positivo, a adiciona a
lista que será retornada. Em caso negativo, é verificado se o tipo implementa a interface Collection e, em
caso positivo, é recuperado o tipo genérico e no qual
também é verificado se possui a anotação @Entity.
Listagem 15. Método que retorna as entidades relacionadas, procurando nos tipos genéricos.
public class Relacoes {
public static List<Class>
getEntidadesRelacionadas(Class c){
List<Class> list = new ArrayList<>();
for(Field f : c.getDeclaredFields()){
Class tipo = f.getType();
if(tipo.isAnnotationPresent(Entity.class)){
list.add(tipo);
}else if(Collection.class.isAssignableFrom(tipo)){
/ 10
}
public static void main(String[] args) {
List<Class> lista =
Relacoes.getEntidadesRelacionadas(Pessoa.class);
for(Class c : lista){
System.out.println(c.getName());
}
}
Outro cenário onde a recuperação do parâmetro genérico por reflexão pode ser muito útil é em relação
ao tipo utilizado na superclasse. Imagine o exemplo
de uma abstração para a representação de um DAO,
como anteriormente apresentado na Listagem 2 deste artigo. Muitas vezes, é preciso saber qual é a classe
que será persistida para implementação dos métodos,
principalmente se a implementação for JPA. Dessa
forma, a superclasse pode disponibilizar um método
para a recuperação do parâmetro genérico utilizado
na subclasse.
A Listagem 17 apresenta como esse método pode
ser implementado. A partir da classe do objeto, é feita
uma busca nas superclasses até que seja encontrada a
superclasse DAO. Então, o método getGenericSuperclass() é utilizado para recuperar uma instância de
ParameterizedType e a partir dele serem recuperados
os parâmetros genéricos.
Listagem 17. Exemplo de uma superclasse com tipo
genérico que fornece método para recuperação do
parâmetro genérico configurado.
public abstract class DAO<E> {
public abstract void salvar(E obj);
public abstract E recuperar(int id);
public abstract List<E> listarTodos();
}
public Class getGenericParameter(){
Class c = this.getClass();
while(!c.getSuperclass().equals(DAO.class)){
c = c.getSuperclass();
}
ParameterizedType tipoGenerico =
(ParameterizedType) c.getGenericSuperclass();
return
(Class) tipoGenerico.getActualTypeArguments()[0];
}
Para completar o exemplo, a Listagem 18 mostra
a classe DAOPessoa que estende a classe DAO com o
tipo genérico Pessoa. O método main() cria uma instância classe e invoca o método getGenericParameter() criado. Com a execução desse código, o nome da
classe Pessoa será impresso no console.
Listagem 18. Classe que estende a superclasse genérica e retorna a classe do parâmetro genérico.
public class PessoaDAO extends DAO<Pessoa> {
public static void main(String[] args) {
PessoaDAO dao = new PessoaDAO();
System.out.println(dao.getGenericParameter().
getName());
}
//implementações dos métodos abstratos omitidas
}
Considerações finais
Este artigo teve como objetivo fazer uma revisão
dos tipos genéricos e apresentar algumas utilizações
desse recurso que são pouco exploradas pelos desenvolvedores. Dentre as técnicas apresentadas está
a restrição no parâmetro genérico de uma classe ou
método, utilização de exceções genéricas e a recuperação do tipo genérico em declarações, como de atributos, de métodos e de classe.
Além de ser interessante e curioso a utilização de
um recurso de linguagem pouco conhecido, ou mesmo de uma forma que foge do convencional, este artigo também discutiu os cenários em que cada uma
dessas técnicas poderiam ser utilizadas. Através dos
exemplos, foram mostrados cenários cotidianos de
aplicações que ilustraram o contexto para o seu uso.
Design Patterns com Java:
projeto orientado a objetos guiado
por padrões
Se você se interessou por este artigo e gosta
de estudar sobre técnicas de modelagem de
software, acabou de ser lançado pela editora
Casa do Código o livro “Design Patterns com
Java: Projeto orientado a objetos guiado por
padrões”. Esse livro apresenta, de uma forma
didática e moderna, como utilizar os padrões
de projeto para um bom design orientado a
objetos em Java. Além dos padrões em si, o livro traz exemplos de APIs e frameworks Java
que os utilizam. Adicionalmente, também são
abordadas técnicas de design modernas, como
o uso de interface fluente, componentes plugáveis e criação de frameworks. Dentro do
contexto deste artigo, é abordado no último
capítulo como os tipos genéricos podem ser
utilizados na implementação dos padrões.
11 \
Download

Coisas Que Você Não Sabia sobre Generics