As Genéricos em Java é um dos recursos mais importantes introduzidos no Java 5. Se você esteve trabalhando com Coleções em Java e com a versão 5 ou superior, tenho certeza de que você já o utilizou. Genéricos em Java com classes de coleção é muito fácil, mas oferece muito mais recursos do que apenas criar o tipo de coleção. Tentaremos aprender os recursos dos genéricos neste artigo. Entender os genéricos pode se tornar confuso às vezes se usarmos palavras técnicas, então tentarei mantê-lo simples e fácil de entender.
Vamos examinar os seguintes tópicos sobre genéricos em Java.
1. Genéricos em Java
Genéricos foram adicionados no Java 5 para fornecer verificação de tipo em tempo de compilação e remover o risco de ClassCastException que era comum ao trabalhar com classes de coleção. Todo o framework de coleção foi reescrito para usar genéricos para garantir o tipo. Vamos ver como os genéricos nos ajudam a usar as classes de coleção com segurança.
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
//casting de tipo levando a ClassCastException em tempo de execução
String str=(String) obj;
}
O código acima compila bem, mas lança ClassCastException em tempo de execução porque estamos tentando converter Object na lista para String, enquanto um dos elementos é do tipo Integer. Após o Java 5, usamos classes de coleção como abaixo.
List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); //erro de compilação
for(String str : list1){
//nenhum tipo de conversão necessário, evita ClassCastException
}
Observe que no momento da criação da lista, especificamos que o tipo de elementos na lista será String. Portanto, se tentarmos adicionar qualquer outro tipo de objeto na lista, o programa lançará um erro de compilação. Observe também que no loop for, não precisamos fazer a conversão de tipo do elemento na lista, removendo assim o ClassCastException em tempo de execução.
2. Classe Genérica em Java
Podemos definir nossas próprias classes com tipos genéricos. Um tipo genérico é uma classe ou interface que é parametrizada sobre tipos. Usamos colchetes angulares (<>) para especificar o parâmetro de tipo. Para entender o benefício, vamos dizer que temos uma classe simples como:
package com.journaldev.generics;
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
Observe que ao usar essa classe, temos que usar conversão de tipo e isso pode produzir ClassCastException em tempo de execução. Agora vamos usar uma classe genérica java para reescrever a mesma classe como mostrado abaixo.
package com.journaldev.generics;
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
Observe o uso da classe GenericsType no método principal. Não precisamos fazer conversão de tipo e podemos remover ClassCastException em tempo de execução. Se não fornecermos o tipo no momento da criação, o compilador produzirá um aviso de que “GenericsType é um tipo bruto. Referências ao tipo genérico GenericsType<T> devem ser parametrizadas”. Quando não fornecemos o tipo, o tipo se torna Object
e, portanto, está permitindo tanto objetos String quanto Integer. Mas, devemos sempre tentar evitar isso porque teremos que usar conversão de tipo ao trabalhar com tipos brutos que podem produzir erros em tempo de execução.
Dica: Podemos usar @SuppressWarnings("rawtypes")
anotação para suprimir o aviso do compilador, confira o tutorial de anotações java.
Observe também que ele suporta autoboxing java.
3. Interface Genérica do Java
A interface Comparable é um ótimo exemplo de Generics em interfaces e é escrita como:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
Da mesma forma, podemos criar interfaces genéricas em Java. Também podemos ter múltiplos parâmetros de tipo, como na interface Map. Novamente, podemos fornecer um valor parametrizado para um tipo parametrizado também, por exemplo new HashMap<String, List<String>>();
é válido.
4. Tipo Genérico do Java
A convenção de nomenclatura de tipos genéricos do Java ajuda-nos a entender o código facilmente e ter uma convenção de nomenclatura é uma das melhores práticas da linguagem de programação Java. Portanto, os genéricos também vêm com suas próprias convenções de nomenclatura. Geralmente, os nomes dos parâmetros de tipo são letras maiúsculas únicas para torná-los facilmente distinguíveis das variáveis Java. Os nomes de parâmetros de tipo mais comumente usados são:
- E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
- K – Key (Used in Map)
- N – Number
- T – Type
- V – Value (Used in Map)
- S,U,V etc. – 2nd, 3rd, 4th types
5. Método Genérico do Java
Às vezes, não queremos que toda a classe seja parametrizada, nesse caso, podemos criar um método genérico em Java. Já que o construtor é um tipo especial de método, também podemos usar tipos genéricos em construtores. Aqui está uma classe mostrando um exemplo de método genérico em Java.
package com.journaldev.generics;
public class GenericsMethods {
//Método Genérico em Java
public static boolean isEqual(GenericsType g1, GenericsType g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.isEqual(g1, g2);
//A declaração acima pode ser escrita de forma mais simples como
isEqual = GenericsMethods.isEqual(g1, g2);
//Essa funcionalidade, conhecida como inferência de tipo, permite que você invoque um método genérico como um método comum, sem especificar um tipo entre colchetes angulares.
//O compilador inferirá o tipo que é necessário
}
}
Observe a assinatura do método isEqual mostrando a sintaxe para usar tipos genéricos em métodos. Também observe como usar esses métodos em nosso programa Java. Podemos especificar o tipo ao chamar esses métodos ou podemos invocá-los como um método normal. O compilador Java é inteligente o suficiente para determinar o tipo de variável a ser usado, essa facilidade é chamada de inferência de tipo.
6. Parâmetros de Tipo Limitados em Java Generics
Suponha que desejamos restringir o tipo de objetos que podem ser usados no tipo parametrizado, por exemplo, em um método que compara dois objetos e queremos garantir que os objetos aceitos sejam Comparables. Para declarar um parâmetro de tipo limitado, liste o nome do parâmetro de tipo, seguido pela palavra-chave extends, seguida pelo seu limite superior, semelhante ao método abaixo.
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
A invocação desses métodos é semelhante ao método não limitado, exceto que se tentarmos usar qualquer classe que não seja Comparable, ocorrerá um erro de compilação. Os parâmetros de tipo limitado podem ser usados com métodos, bem como classes e interfaces. Os Genéricos do Java também suportam múltiplos limites, ou seja,
7. Java Generics e Herança
Sabemos que a herança do Java nos permite atribuir uma variável A a outra variável B se A for subclasse de B. Portanto, podemos pensar que qualquer tipo genérico de A pode ser atribuído ao tipo genérico de B, mas não é o caso. Vamos ver isso com um programa simples.
package com.journaldev.generics;
public class GenericsInheritance {
public static void main(String[] args) {
String str = "abc";
Object obj = new Object();
obj=str; // works because String is-a Object, inheritance in java
MyClass myClass1 = new MyClass();
MyClass
Não é permitido atribuir uma variável MyClass<String> a uma variável MyClass<Object> porque elas não estão relacionadas, na verdade o pai de MyClass<T> é Object.
8. Classes Genéricas e Subtipagem em Java
Podemos criar subtipos de uma classe ou interface genérica estendendo ou implementando-a. A relação entre os parâmetros de tipo de uma classe ou interface e os parâmetros de tipo de outra é determinada pelas cláusulas extends e implements. Por exemplo, ArrayList<E> implementa List<E> que estende Collection<E>, então ArrayList<String> é um subtipo de List<String> e List<String> é subtipo de Collection<String>. A relação de subtipagem é preservada enquanto não alteramos o argumento de tipo, abaixo mostra um exemplo de múltiplos parâmetros de tipo.
interface MyList<E,T> extends List<E>{
}
Os subtipos de List<String> podem ser MyList<String,Object>, MyList<String,Integer> e assim por diante.
9. Wildcards em Java Generics
O ponto de interrogação (?) é o caractere coringa em genéricos e representa um tipo desconhecido. O caractere coringa pode ser usado como o tipo de um parâmetro, campo ou variável local e às vezes como um tipo de retorno. Não podemos usar caracteres coringa ao chamar um método genérico ou instanciar uma classe genérica. Nas seções seguintes, aprenderemos sobre caracteres coringa delimitados superiormente, caracteres coringa delimitados inferiormente e a captura de caracteres coringa.
9.1) Caractere Coringa Delimitado Superiormente em Genéricos Java
Os caracteres coringa delimitados superiormente são usados para relaxar a restrição no tipo de variável em um método. Suponha que queiramos escrever um método que retorne a soma dos números na lista, então nossa implementação será algo assim.
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
Agora, o problema com a implementação acima é que ela não funcionará com uma Lista de Inteiros ou Doubles porque sabemos que List<Integer> e List<Double> não estão relacionadas, é aí que um caractere coringa delimitado superiormente é útil. Usamos o caractere coringa genérico com a palavra-chave extends e a classe ou interface delimitadora superior que nos permitirá passar argumentos do tipo delimitado superior ou de suas subclasses. A implementação acima pode ser modificada como no programa abaixo.
package com.journaldev.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
É semelhante a escrever nosso código em termos de interface, no método acima podemos usar todos os métodos da classe de limite superior Number. Note que com uma lista delimitada superior, não é permitido adicionar nenhum objeto à lista, exceto null. Se tentarmos adicionar um elemento à lista dentro do método sum, o programa não será compilado.
9.2) Java Generics Unbounded Wildcard
Às vezes, temos uma situação em que queremos que nosso método genérico funcione com todos os tipos; nesse caso, um wildcard não limitado pode ser usado. É o mesmo que usar <? extends Object>.
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
Podemos fornecer List<String> ou List<Integer> ou qualquer outro tipo de argumento de lista de objeto para o método printData. Semelhante à lista delimitada superior, não é permitido adicionar nada à lista.
9.3) Java Generics Lower bounded Wildcard
Suponha que desejamos adicionar inteiros a uma lista de inteiros em um método, podemos manter o tipo de argumento como List<Integer>, mas isso estará vinculado apenas a Integers, enquanto List<Number> e List<Object> também podem conter inteiros, então podemos usar um caractere curinga de limite inferior para conseguir isso. Usamos o caractere curinga (?) com a palavra-chave super e a classe de limite inferior para alcançar isso. Podemos passar um limite inferior ou qualquer supertipo do limite inferior como argumento, nesse caso, o compilador Java permite adicionar tipos de objeto de limite inferior à lista.
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
10. Subtipagem usando Curingas Genéricos
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
11. Apagamento de Tipos em Generics Java
Generics em Java foram adicionados para fornecer verificação de tipo em tempo de compilação e não têm uso em tempo de execução, então o compilador Java usa o recurso de apagamento de tipos para remover todo o código de verificação de tipo genérico no código de bytes e inserir conversões de tipo se necessário. O apagamento de tipos garante que nenhuma nova classe seja criada para tipos parametrizados; consequentemente, os genéricos não incorrem em sobrecarga de tempo de execução. Por exemplo, se tivermos uma classe genérica como abaixo;
public class Test<T extends Comparable<T>> {
private T data;
private Test<T> next;
public Test(T d, Test<T> n) {
this.data = d;
this.next = n;
}
public T getData() { return this.data; }
}
O compilador Java substitui o parâmetro de tipo limitado T pelo primeiro limite da interface, Comparable, como no código abaixo:
public class Test {
private Comparable data;
private Test next;
public Node(Comparable d, Test n) {
this.data = d;
this.next = n;
}
public Comparable getData() { return data; }
}
12. Perguntas frequentes sobre Genéricos
12.1) Por que usamos Genéricos em Java?
Os Genéricos fornecem uma verificação de tipo forte em tempo de compilação e reduzem o risco de ClassCastException e de conversão explícita de objetos.
12.2) O que é T em Genéricos?
Usamos <T> para criar uma classe, interface e método genéricos. O T é substituído pelo tipo real quando o utilizamos.
12.3) Como funcionam os Genéricos em Java?
O código genérico garante a segurança de tipos. O compilador utiliza a eliminação de tipo (type-erasure) para remover todos os parâmetros de tipo no momento da compilação, reduzindo a sobrecarga em tempo de execução.
13. Genéricos em Java – Leituras Adicionais
- Os genéricos não suportam subtipagem, então
List<Number> números = new ArrayList<Integer>();
não irá compilar, aprenda por que os genéricos não suportam subtipagem. - Não podemos criar arrays genéricos, então
List<Integer>[] array = new ArrayList<Integer>[10]
não irá compilar, leia por que não podemos criar arrays genéricos?.
Isso é tudo para genéricos em Java, os genéricos em Java são um tópico realmente vasto e exigem muito tempo para entender e usá-los efetivamente. Este post aqui é uma tentativa de fornecer detalhes básicos sobre genéricos e como podemos usá-los para estender nosso programa com segurança de tipo.
Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface