Exemplo de Tutorial de Genéricos em Java – Método Genérico, Classe, Interface

As Genéricos Java são uma das características mais importantes introduzidas no Java 5. Se você tem trabalhado com Coleções Java e com a versão 5 ou superior, tenho certeza de que você já a utilizou. Genéricos em Java com classes de coleção são muito fáceis, mas eles oferecem muito mais recursos do que apenas criar o tipo de coleção. Tentaremos aprender os recursos dos genéricos neste artigo. Entender 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 analisar os seguintes tópicos de genéricos em Java.

  1. Genéricos em Java

  2. Classe Genérica Java

  3. Interface Genérica Java

  4. Tipo Genérico Java

  5. Método Genérico Java

  6. Parâmetros de Tipo Delimitado em Genéricos Java

  7. Genéricos em Java e Herança

  8. Classes Genéricas em Java e Subtipagem

  9. Curingas em Genéricos de Java

  10. Curinga de limite superior em Genéricos de Java

  11. Curinga de limite inferior em Genéricos de Java

  12. Curinga de limite inferior em Genéricos de Java

  13. Subtipagem usando Curingas em Genéricos

  14. Ocultação de Tipo em Genéricos de Java

  15. Perguntas Frequentes sobre Genéricos

1. Genéricos em Java

Os 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 segurança de 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){
	//cast 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 fazer cast de 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 cast necessário, evita ClassCastException
}

Observe que no momento da criação da lista, especificamos que o tipo dos 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 cast do elemento na lista, removendo assim a 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 esta classe, temos que fazer um casting de tipo e isso pode produzir ClassCastException em tempo de execução. Agora vamos usar uma classe genérica em 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 o casting de tipo e podemos evitar 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, permite tanto objetos String quanto Integer. No entanto, devemos sempre tentar evitar isso porque teremos que usar casting de tipo ao trabalhar com um tipo bruto que pode produzir erros em tempo de execução.

Dica: Podemos usar a anotação @SuppressWarnings("rawtypes") para suprimir o aviso do compilador, confira o tutorial de anotações em Java.

Também observe que ele suporta autoboxing em 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);
}

De forma semelhante, 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 do Tipo Genérico do Java nos ajuda 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. Como o construtor é um tipo especial de método, também podemos usar tipos genéricos nos construtores. Aqui está uma classe mostrando um exemplo de um método genérico em Java.

package com.journaldev.generics;

public class GenericsMethods {

	// Método Genérico 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 simplesmente como
		isEqual = GenericsMethods.isEqual(g1, g2);
		// Esse recurso, conhecido 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 necessário
	}
}

Observe a assinatura do método isEqual mostrando a sintaxe para usar tipos genéricos em métodos. Além disso, 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 Genérico em Java

Suponhamos 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 delimitado, 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 delimitado, exceto que se tentarmos usar qualquer classe que não seja Comparable, será gerado um erro de compilação. Parâmetros de tipo delimitados podem ser usados com métodos, assim como com classes e interfaces. Os Genéricos do Java também suportam múltiplos limites, ou seja, . Neste caso, A pode ser uma interface ou classe. Se A for uma classe, então B e C devem ser interfaces. Não podemos ter mais de uma classe em múltiplos limites.

7. Genéricos do Java e Herança

Sabemos que a herança em Java nos permite atribuir uma variável A a outra variável B se A for uma subclasse de B. Portanto, poderíamos 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 myClass2 = new MyClass();
		//myClass2=myClass1; // erro de compilação, já que MyClass não é um MyClass
		obj = myClass1; // MyClass parent is Object
	}
	
	public static class 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 e Subtipagem Genéricas em Java

Podemos criar um subtipo de uma classe ou interface genérica estendendo-a ou implementando-a. O relacionamento entre os parâmetros de tipo de uma classe ou interface e os parâmetros de tipo de outra são determinados 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>. O relacionamento de subtipagem é preservado enquanto não alterarmos 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. Curelas Genéricas em Java

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 invocar um método genérico ou instanciar uma classe genérica. Nas seções a seguir, aprenderemos sobre caracteres coringa delimitados por superior, caracteres coringa delimitados por inferior e a captura de caracteres coringa.

9.1) Caracteres Coringa Delimitados por Superior em Java

Caracteres coringa delimitados por superior são usados para relaxar a restrição sobre o tipo de variável em um método. Suponhamos que queiramos escrever um método que retorne a soma dos números na lista; portanto, 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 Listas de Inteiros ou Dobráveis, porque sabemos que List e List não estão relacionadas. É aí que um caractere coringa delimitado por superior é útil. Usamos o caractere coringa genérico com a palavra-chave extends e a classe ou interface de limite superior que nos permitirá passar argumentos de tipo de limite 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 limitada superior, não é permitido adicionar nenhum objeto à lista, exceto null. Se tentarmos adicionar um elemento à lista dentro do método de soma, o programa não irá compilar.

9.2) Genéricos Java Curinga Não Limitado

Às vezes, temos uma situação em que queremos que nosso método genérico funcione com todos os tipos, nesse caso, um curinga 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 de limite superior, não é permitido adicionar nada à lista.

9.3) Genéricos Java Curinga Limitado Inferior

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 aos Integers, enquanto List<Number> e List<Object> também podem conter inteiros, então podemos usar um curinga de limite inferior para conseguir isso. Usamos o curinga de generics (?) 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 objetos de limite inferior à lista.

public static void addIntegers(List<? super Integer> list){
		list.add(new Integer(50));
	}

10. Subtipagem usando Curingas Generics

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 Generics em 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 generics no código de bytes e inserir casting de tipo, se necessário. O apagamento de tipos garante que nenhuma nova classe seja criada para tipos parametrizados; consequentemente, os generics 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 delimitado 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 proporcionam 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 tipo. O compilador utiliza o apagamento de tipo 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

Isso é tudo para genéricos em java, os genéricos em Java são um tópico realmente vasto e requerem bastante tempo para serem compreendidos e utilizados de forma eficaz. 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