Noções básicas de genéricos Java
*1. Introdução *
Os Java Generics foram introduzidos no JDK 5.0 com o objetivo de reduzir bugs e adicionar uma camada extra de abstração sobre os tipos.
Este artigo é uma introdução rápida aos genéricos em Java, o objetivo por trás deles e como eles podem ser usados para melhorar a qualidade do nosso código.
Leitura adicional:
https://www..com/java-method-references [referências de método em Java]
Uma visão geral rápida e prática das referências de métodos em Java.
https://www..com/java-method-references [Leia mais] →
https://www..com/java-reflection-class-fields [Recuperar campos de uma classe Java usando reflexão]
Aprenda como obter os campos de uma classe usando reflexão, incluindo campos herdados
https://www..com/java-reflection-class-fields [Leia mais] →
===* 2. A necessidade de genéricos *
Vamos imaginar um cenário em que queremos criar uma lista em Java para armazenar Integer; podemos ser tentados a escrever:
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
Surpreendentemente, o compilador reclamará da última linha. Ele não sabe que tipo de dados é retornado. O compilador exigirá uma conversão explícita:
Integer i = (Integer) list.iterator.next();
Não há contrato que garanta que o tipo de retorno da lista seja Integer. A lista definida pode conter qualquer objeto. Só sabemos que estamos recuperando uma lista inspecionando o contexto. Ao examinar tipos, ele pode garantir apenas que é um Object, portanto, exige uma conversão explícita para garantir que o tipo seja seguro.
Esse elenco pode ser irritante, sabemos que o tipo de dados nesta lista é um Integer. O elenco também está bagunçando nosso código. Pode causar erros de tempo de execução relacionados ao tipo se um programador cometer um erro com a conversão explícita.
Seria muito mais fácil se os programadores pudessem expressar sua intenção de usar tipos específicos e o compilador garantir a correção desse tipo. Essa é a idéia principal por trás dos genéricos.
Vamos modificar a primeira linha do snippet de código anterior para:
List<Integer> list = new LinkedList<>();
Ao adicionar o operador de diamante <> que contém o tipo, restringimos a especialização desta lista apenas ao tipo Integer, ou seja, nós especificamos o tipo que será mantido dentro da lista. O compilador pode aplicar o tipo em tempo de compilação.
Em programas pequenos, isso pode parecer uma adição trivial; no entanto, em programas maiores, isso pode adicionar robustez significativa e facilitar a leitura do programa.
===* 3. Métodos genéricos *
Métodos genéricos são aqueles que são escritos com uma única declaração de método e podem ser chamados com argumentos de tipos diferentes. O compilador garantirá a correção de qualquer tipo usado. Estas são algumas propriedades de métodos genéricos:
-
Os métodos genéricos têm um parâmetro de tipo (o operador diamante que encerra o tipo) antes do tipo de retorno da declaração do método
-
Os parâmetros de tipo podem ser limitados (os limites são explicados mais adiante neste artigo)
-
Métodos genéricos podem ter diferentes parâmetros de tipo separados por vírgulas na assinatura do método *O corpo do método para um método genérico é como um método normal
Um exemplo de definição de um método genérico para converter uma matriz em uma lista:
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
No exemplo anterior, o _ <T> _ na assinatura do método implica que o método estará lidando com o tipo genérico T. Isso é necessário mesmo se o método estiver retornando nulo.
Como mencionado acima, o método pode lidar com mais de um tipo genérico; nesse caso, todos os tipos genéricos devem ser adicionados à assinatura do método, por exemplo, se quisermos modificar o método acima para lidar com o tipo T e o tipo G, deve ser escrito assim:
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
Estamos passando uma função que converte uma matriz com os elementos do tipo T para listar com elementos do tipo G. Um exemplo seria converter Integer na sua representação String:
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
Vale ressaltar que a recomendação da Oracle é usar uma letra maiúscula para representar um tipo genérico e escolher uma letra mais descritiva para representar tipos formais, por exemplo, em Java Collections T é usado para o tipo, K para a chave, V para o valor.
====* 3.1 Genéricos limitados *
Como mencionado anteriormente, os parâmetros de tipo podem ser limitados. Limite significa "restricted", podemos restringir tipos que podem ser aceitos por um método.
Por exemplo, podemos especificar que um método aceita um tipo e todas as suas subclasses (limite superior) ou um tipo todas as suas superclasses (limite inferior).
Para declarar um tipo de limite superior, usamos a palavra-chave extends após o tipo seguido pelo limite superior que queremos usar. Por exemplo:
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
A palavra-chave extends é usada aqui para significar que o tipo T estende o limite superior no caso de uma classe ou implementa um limite superior no caso de uma interface.
====* 3.2 Vários limites *
Um tipo também pode ter vários limites superiores da seguinte maneira:
<T extends Number & Comparable>
Se um dos tipos estendidos por T for uma classe (ou seja, Number), ele deverá ser colocado em primeiro lugar na lista de limites. Caso contrário, isso causará um erro em tempo de compilação.
===* 4. Usando curingas com genéricos *
Os curingas são representados pelo ponto de interrogação no Java “_? _” E são usados para se referir a um tipo desconhecido. Os curingas são particularmente úteis ao usar genéricos e podem ser usados como um tipo de parâmetro, mas primeiro, há uma observação importante para considerar.
*Sabe-se que _Object_ é o supertipo de todas as classes Java; no entanto, uma coleção de _Object_ não é o supertipo de nenhuma coleção.*
Por exemplo, um _List <Object> _ não é o supertipo de _List <String> _ e atribuir uma variável do tipo _List <Object> _ a uma variável do tipo _List <String> _ causará um erro do compilador. Isso evita possíveis conflitos que podem acontecer se adicionarmos tipos heterogêneos à mesma coleção.
A mesma regra se aplica a qualquer coleção de um tipo e seus subtipos. Considere este exemplo:
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
se imaginarmos um subtipo de Building, por exemplo, uma House, não podemos usar esse método com uma lista de House, mesmo que House seja um subtipo de Building. Se precisarmos usar esse método com o tipo Building e todos os seus subtipos, o curinga limitado poderá fazer a mágica:
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
Agora, esse método funcionará com o tipo Building e todos os seus subtipos. Isso é chamado de curinga com limite superior, onde o tipo Building é o limite superior.
Os curingas também podem ser especificados com um limite inferior, em que o tipo desconhecido deve ser um supertipo do tipo especificado. Limites inferiores podem ser especificados usando a palavra-chave super seguida pelo tipo específico, por exemplo, _ <? super T> _ significa tipo desconhecido que é uma superclasse de T (= T e todos os seus pais).
*5. Tipo Apagamento *
Os genéricos foram adicionados ao Java para garantir a segurança do tipo e para garantir que os genéricos não causassem sobrecarga no tempo de execução, o compilador aplica um processo chamado type apagamento nos genéricos em tempo de compilação.
O apagamento de tipo remove todos os parâmetros de tipo e o substitui por seus limites ou por Object se o parâmetro de tipo não tiver limites. Portanto, o bytecode após a compilação contém apenas classes, interfaces e métodos normais, garantindo assim que nenhum novo tipo seja produzido. A conversão adequada também é aplicada ao tipo Object no momento da compilação.
Este é um exemplo do tipo apagamento:
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
Com o apagamento do tipo, o tipo ilimitado T é substituído por Object da seguinte maneira:
//for illustration
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
//which in practice results in
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
Se o tipo for delimitado, o tipo será substituído pelo delimitado no tempo de compilação:
public <T extends Building> void genericMethod(T t) {
...
}
mudaria após a compilação:
public void genericMethod(Building t) {
...
}
===* 6. Tipos de dados genéricos e primitivos *
*Uma restrição de genéricos em Java é que o parâmetro type não pode ser um tipo primitivo.
Por exemplo, o seguinte não é compilado:
List<int> list = new ArrayList<>();
list.add(17);
Para entender por que os tipos de dados primitivos não funcionam, lembre-se de que* genéricos são um recurso em tempo de compilação *, o que significa que o parâmetro type é apagado e todos os tipos genéricos são implementados como o tipo Object.
Como exemplo, vejamos o método add de uma lista:
List<Integer> list = new ArrayList<>();
list.add(17);
A assinatura do método add é:
boolean add(E e);
E será compilado para:
boolean add(Object e);
Portanto, os parâmetros de tipo devem ser conversíveis em Object. Como os tipos primitivos não estendem Object, não podemos usá-los como parâmetros de tipo.
No entanto, o Java fornece tipos de caixa para primitivos, juntamente com a caixa automática e a unboxing para desembrulhá-las:
Integer a = 17;
int b = a;
Portanto, se queremos criar uma lista que possa conter números inteiros, podemos usar o wrapper:
List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);
O código compilado será o equivalente a:
List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();
*Versões futuras do Java podem permitir tipos de dados primitivos para genéricos.* O projeto https://openjdk.java.net/projects/valhalla/[Valhalla] visa melhorar a maneira como os genéricos são manipulados. A idéia é implementar a especialização em genéricos, conforme descrito em https://openjdk.java.net/jeps/218[JEP 218].
7. Conclusão
O Java Generics é uma adição poderosa à linguagem Java, pois torna o trabalho do programador mais fácil e menos propenso a erros. Os genéricos reforçam a correção de tipo no tempo de compilação e, mais importante, permitem a implementação de algoritmos genéricos sem causar sobrecarga extra em nossos aplicativos.
O código fonte que acompanha o artigo está disponível over no GitHub.