Introdução
A partir do JSE 5.0, os genéricos foram adicionados ao arsenal da linguagem Java.O que são genéricos em Java?
Genéricos (generais) são meios especiais da linguagem Java para implementar programação generalizada: uma abordagem especial para descrever dados e algoritmos que permite trabalhar com diferentes tipos de dados sem alterar sua descrição. No site da Oracle, um tutorial separado é dedicado aos genéricos: “ Lição: Genéricos ”.
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
Este código funcionará bem. Mas e se eles viessem até nós e dissessem que a frase “Olá, mundo!” espancado e você só pode retornar Olá? Vamos remover a concatenação com a string do código ", world!"
. Parece que o que poderia ser mais inofensivo? Mas, na verdade, receberemos um erro DURANTE A COMPILAÇÃO : error: incompatible types: Object cannot be converted to String
Acontece que no nosso caso List armazena uma lista de objetos do tipo Object. Como String é descendente de Object (já que todas as classes são herdadas implicitamente de Object em Java), ela requer uma conversão explícita, o que não fizemos. E ao concatenar, o método estático String.valueOf(obj) será chamado no objeto, que por fim chamará o método toString no Objeto. Ou seja, nossa Lista contém Object. Acontece que onde precisarmos de um tipo específico, e não de Object, teremos que fazer nós mesmos a conversão do tipo:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
Porém, neste caso, porque List aceita uma lista de objetos, armazena não apenas String, mas também Integer. Mas o pior é que neste caso o compilador não verá nada de errado. E aqui receberemos um erro DURANTE A EXECUÇÃO (dizem também que o erro foi recebido “em Runtime”). O erro será: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Concordo, não é o mais agradável. E tudo isso porque o compilador não é inteligência artificial e não consegue adivinhar tudo o que o programador quer dizer. Para informar mais ao compilador sobre quais tipos usaremos, o Java SE 5 introduziu genéricos . Vamos corrigir nossa versão dizendo ao compilador o que queremos:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
Como podemos ver, não precisamos mais da conversão para String. Além disso, agora temos colchetes angulares que enquadram os genéricos. Agora o compilador não permitirá que a classe seja compilada até removermos a adição de 123 à lista, porque isso é inteiro. Ele nos dirá isso. Muitas pessoas chamam os genéricos de “açúcar sintático”. E eles estão certos, já que os genéricos realmente se tornarão essas mesmas castas quando compilados. Vejamos o bytecode das classes compiladas: com conversão manual e usando genéricos:
Tipos brutos ou tipos brutos
Quando falamos de genéricos, sempre temos duas categorias: tipos digitados (Generic Types) e tipos “brutos” (Raw Types). Tipos brutos são tipos sem especificar o “qualificador” entre colchetes angulares:<>
a sintaxe do losango também está associada ao conceito de " Inferência de tipo ", ou inferência de tipo. Afinal, o compilador, vendo <> à direita, olha para o lado esquerdo, onde está localizada a declaração do tipo da variável à qual o valor está atribuído. E a partir dessa parte ele entende qual tipo é digitado o valor da direita. Na verdade, se um genérico for especificado no lado esquerdo e não especificado no lado direito, o compilador poderá inferir o tipo:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
No entanto, isso seria uma mistura do novo estilo com os genéricos e do estilo antigo sem eles. E isso é extremamente indesejável. Ao compilar o código acima receberemos a mensagem: Note: HelloWorld.java uses unchecked or unsafe operations
. Na verdade, não parece claro por que é necessário adicionar diamantes aqui. Mas aqui está um exemplo:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
Como lembramos, ArrayList também possui um segundo construtor que recebe uma coleção como entrada. E é aqui que reside o engano. Sem a sintaxe do diamante, o compilador não entende que está sendo enganado, mas com o diamante ele entende. Portanto, regra nº 1 : sempre use a sintaxe diamante se usarmos tipos digitados. Caso contrário, corremos o risco de perder onde usamos o tipo bruto. Para evitar avisos no log que “utilizam operações não verificadas ou inseguras” você pode especificar uma anotação especial no método ou classe que está sendo usada: @SuppressWarnings("unchecked")
Suppress é traduzido como suprimir, ou seja, literalmente, suprimir avisos. Mas pense por que você decidiu indicá-lo? Lembre-se da regra número um e talvez você precise adicionar digitação.
Métodos Genéricos
Os genéricos permitem que você digite métodos. Há uma seção separada dedicada a esse recurso no tutorial do Oracle: “ Métodos Genéricos ”. Neste tutorial, é importante lembrar a sintaxe:- inclui uma lista de parâmetros digitados entre colchetes angulares;
- a lista de parâmetros digitados vem antes do método retornado.
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
Se você observar a classe Util, veremos dois métodos digitados nela. Com a inferência de tipo, podemos fornecer a definição do tipo diretamente ao compilador ou podemos especificá-la nós mesmos. Ambas as opções são apresentadas no exemplo. A propósito, a sintaxe é bastante lógica se você pensar bem. Ao digitar um método, especificamos o genérico ANTES do método porque se usarmos o genérico depois do método, Java não será capaz de descobrir qual tipo usar. Portanto, primeiro anunciamos que usaremos o genérico T, e depois dizemos que retornaremos esse genérico. Naturalmente, Util.<Integer>getValue(element, String.class)
falhará com um erro incompatible types: Class<String> cannot be converted to Class<Integer>
. Ao usar métodos digitados, você deve sempre se lembrar do apagamento de tipo. Vejamos um exemplo:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
Funcionará muito bem. Mas apenas desde que o compilador entenda que o método chamado possui um tipo Integer. Vamos substituir a saída do console pela seguinte linha: System.out.println(Util.getValue(element) + 1);
E obtemos o erro: tipos de operandos incorretos para o operador binário '+', primeiro tipo: Object , segundo tipo: int Ou seja, os tipos foram apagados. O compilador vê que ninguém especificou o tipo, o tipo é especificado como Object e a execução do código falha com um erro.
Tipos genéricos
Você pode digitar não apenas métodos, mas também as próprias classes. A Oracle tem uma seção “ Tipos Genéricos ” dedicada a isso em seu guia. Vejamos um exemplo:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
Tudo é simples aqui. Se usarmos uma classe, o genérico será listado após o nome da classe. Vamos agora criar uma instância desta classe no método principal:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Funcionará bem. O compilador vê que existe uma Lista de números e uma Coleção do tipo String. Mas e se apagarmos os genéricos e fizermos isto:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Receberemos o erro: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Digite apagamento novamente. Como a classe não possui mais um genérico, o compilador decide que, como passamos uma List, um método com List<Integer> é mais apropriado. E caímos com um erro. Portanto, regra nº 2: se uma classe for digitada, sempre especifique o tipo no arquivo genérico .
Restrições
Podemos aplicar uma restrição aos tipos especificados em genéricos. Por exemplo, queremos que o contêiner aceite apenas Número como entrada. Esse recurso é descrito no Tutorial do Oracle na seção Parâmetros de tipo limitado . Vejamos um exemplo:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
Como você pode ver, limitamos o tipo genérico à classe/interface Number e seus descendentes. Curiosamente, você pode especificar não apenas uma classe, mas também interfaces. Por exemplo: public static class NumberContainer<T extends Number & Comparable> {
Genéricos também possuem o conceito de Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Eles, por sua vez, são divididos em três tipos:
- Curingas com limite superior - < ? estende o número >
- Curingas ilimitados - < ? >
- Curingas com limite inferior - < ? super inteiro >
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
Mas se você substituir extends por super, tudo ficará bem. Como preenchemos a lista com um valor antes de exibi-la, ela é um consumidor para nós, ou seja, um consumidor. Portanto, usamos super.
Herança
Há outra característica incomum dos genéricos: sua herança. A herança de genéricos é descrita no tutorial do Oracle na seção " Genéricos, Herança e Subtipos ". O principal é lembrar e perceber o seguinte. Não podemos fazer isso:List<CharSequence> list1 = new ArrayList<String>();
Porque a herança funciona de maneira diferente com os genéricos:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Tudo é simples aqui também. List<String> não é descendente de List<Object>, embora String seja descendente de Object.
Final
Então, refrescamos nossa memória dos genéricos. Se raramente forem usados com toda a força, alguns detalhes ficarão fora da memória. Espero que esta breve revisão ajude a refrescar sua memória. E para obter melhores resultados, recomendo fortemente que você se familiarize com os seguintes materiais:- Yuri Tkach: Tipos brutos - Genéricos #1 - Java avançado
- Herança e extensores genéricos - Genéricos #2 - Java Avançado
- Extensão de tipo recursivo - Genéricos #3 - Java avançado
- Alexander Matorin - genéricos não óbvios
- Introdução ao Java. Genéricos. Curingas | Technostream
- O'Reilly: Genéricos e coleções Java
GO TO FULL VERSION