JavaRush /Blogue Java /Random-PT /A teoria dos genéricos em Java ou como colocar parênteses...
Viacheslav
Nível 3

A teoria dos genéricos em Java ou como colocar parênteses na prática

Publicado no grupo Random-PT

Introdução

A partir do JSE 5.0, os genéricos foram adicionados ao arsenal da linguagem Java.
A teoria dos genéricos em Java ou como colocar parênteses na prática - 1

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 ”.

Primeiro, para entender os genéricos, você precisa entender por que eles são necessários e o que fornecem. No tutorial na seção " Por que usar genéricos ?" Diz-se que um dos propósitos é uma verificação mais forte do tipo em tempo de compilação e a eliminação da necessidade de conversão explícita.
A teoria dos genéricos em Java ou como colocar parênteses na prática - 2
Vamos preparar nosso compilador java on-line tutorial favorito para experimentos . Vamos imaginar este código:
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:
A teoria dos genéricos em Java ou como colocar parênteses na prática - 3
Após a compilação, qualquer informação sobre genéricos é apagada. Isso é chamado de "Apagamento de tipo" ou " Apagamento de tipo ". O apagamento de tipo e os genéricos são projetados para fornecer compatibilidade retroativa com versões mais antigas do JDK, ao mesmo tempo que permitem que o compilador auxilie na inferência de tipo em versões mais recentes do Java.
A teoria dos genéricos em Java ou como colocar parênteses na prática - 4

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 teoria dos genéricos em Java ou como colocar parênteses na prática - 5
Os tipos digitados são o oposto, com a indicação de “esclarecimento”:
A teoria dos genéricos em Java ou como colocar parênteses na prática - 6
Como podemos ver, usamos um design incomum, marcado com uma seta na imagem. Esta é uma sintaxe especial que foi adicionada no Java SE 7 e é chamada de " diamante ", que significa diamante. Por que? Você pode fazer uma analogia entre o formato de um losango e o formato das chaves: <> 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.
A teoria dos genéricos em Java ou como colocar parênteses na prática - 7

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.
Vejamos um exemplo:
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.
Теория дженериков в Java or How на практике ставить скобки - 8

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: O chamado princípio Get Put se aplica aos Wildcards . Eles podem ser expressos da seguinte forma:
Теория дженериков в Java or How на практике ставить скобки - 9
Este princípio também é chamado de princípio PECS (Producer Extends Consumer Super). Você pode ler mais sobre Habré no artigo “ Usando curingas genéricos para melhorar a usabilidade da API Java ”, bem como na excelente discussão sobre stackoverflow: “ Usando curingas em Java genérico ”. Aqui está um pequeno exemplo da fonte Java - o método Collections.copy:
Теория дженериков в Java or How на практике ставить скобки - 10
Bem, um pequeno exemplo de como NÃO funcionará:
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:
Теория дженериков в Java or How на практике ставить скобки - 11
E aqui está outro bom exemplo que falhará com um erro:
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: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION