Oi! Continuamos nossa série de lições sobre genéricos. Anteriormente, tivemos uma ideia geral do que são e por que são necessários. Hoje aprenderemos mais sobre alguns dos recursos dos genéricos e como trabalhar com eles. Vamos! Apagamento de tipo - 1Na última lição , falamos sobre a diferença entre tipos genéricos e tipos brutos . Um tipo bruto é uma classe genérica cujo tipo foi removido.

List list = new ArrayList();
Aqui está um exemplo. Aqui não indicamos que tipo de objetos serão colocados em nosso arquivo List. Se tentarmos criar tal Liste adicionar alguns objetos a ele, veremos um aviso no IDEA:

"Unchecked call to add(E) as a member of raw type of java.util.List".
Mas também falamos sobre o fato de que os genéricos apareceram apenas no Java 5. Na época em que esta versão foi lançada, os programadores já haviam escrito um monte de código usando tipos brutos, então esse recurso da linguagem não poderia parar de funcionar, e a capacidade de criar tipos brutos em Java foi preservado. No entanto, o problema acabou por ser mais generalizado. Como você sabe, o código Java é convertido em um formato compilado especial chamado bytecode, que é executado pela máquina virtual Java. Mas se colocarmos informações sobre os parâmetros de tipo no bytecode durante o processo de conversão, isso quebraria todo o código escrito anteriormente, porque não havia parâmetros de tipo antes do Java 5! Ao trabalhar com genéricos, há um conceito muito importante que você precisa lembrar. É chamado de apagamento de tipo. Isso significa que uma classe não contém informações sobre um parâmetro de tipo. Esta informação está disponível apenas durante a compilação e é apagada (torna-se inacessível) antes do tempo de execução. Se você tentar colocar o tipo errado de objeto em seu List<String>, o compilador irá gerar um erro. Isso é exatamente o que os criadores da linguagem desejam alcançar quando criaram genéricos: verificações em tempo de compilação. Mas quando todo o seu código Java se transforma em bytecode, ele não contém mais informações sobre os parâmetros de tipo. Em bytecode, sua List<Cat>lista de gatos não é diferente de List<String>strings. Em bytecode, nada diz que catsé uma lista de Catobjetos. Essas informações são apagadas durante a compilação — apenas o fato de você ter uma List<Object> catslista acabará no bytecode do programa. Vejamos como isso funciona:

public class TestClass<T> {

   private T value1;
   private T value2;

   public void printValues() {
       System.out.println(value1);
       System.out.println(value2);
   }

   public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
       TestClass<T> result = new TestClass<>();
       result.value1 = (T) o1;
       result.value2 = (T) o2;
       return result;
   }

   public static void main(String[] args) {
       Double d = 22.111;
       String s = "Test String";
       TestClass<Integer> test = createAndAdd2Values(d, s);
       test.printValues();
   }
}
Criamos nossa própria TestClassclasse genérica. É bem simples: na verdade é uma pequena "coleção" de 2 objetos, que são armazenados imediatamente quando o objeto é criado. Tem 2 Tcampos. Quando o createAndAdd2Values()método é executado, os dois objetos passados ​​( Object ae Object bdevem ser convertidos para o Ttipo e depois adicionados ao TestClassobjeto. No main()método, criamos um TestClass<Integer>, ou seja, o Integerargumento de tipo substitui o Integerparâmetro de tipo. Também estamos passando a Doublee a Stringpara o createAndAdd2Values()método. Você acha que nosso programa funcionará? Afinal, especificamos Integercomo o argumento de tipo, mas Stringdefinitivamente não pode ser convertido em um Integer! Vamos executar omain()método e confira. Saída do console:

22.111 
Test String
Isso foi inesperado! Por quê isso aconteceu? É o resultado do apagamento de tipos. As informações sobre o Integerargumento de tipo usado para instanciar nosso TestClass<Integer> testobjeto foram apagadas quando o código foi compilado. O campo se torna TestClass<Object> test. Nossos argumentos Doublee Stringforam facilmente convertidos em Objectobjetos (eles não são convertidos em Integerobjetos como esperávamos!) e discretamente adicionados a TestClass. Aqui está outro exemplo simples, mas muito revelador, de apagamento de tipo:

import java.util.ArrayList;
import java.util.List;

public class Main {

   private class Cat {

   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       List<Integer> numbers = new ArrayList<>();
       List<Cat> cats = new ArrayList<>();

       System.out.println(strings.getClass() == numbers.getClass());
       System.out.println(numbers.getClass() == cats.getClass());

   }
}
Saída do console:

true 
true
Parece que criamos coleções com três argumentos de tipos diferentes — String, Integere nossa própria Catclasse. Mas durante a conversão para bytecode, todas as três listas se tornam List<Object>, então quando o programa é executado ele nos diz que estamos usando a mesma classe em todos os três casos.

Apagamento de tipo ao trabalhar com arrays e genéricos

Há um ponto muito importante que deve ser entendido claramente ao trabalhar com arrays e classes genéricas (como List). Você também deve levar isso em consideração ao escolher estruturas de dados para seu programa. Os genéricos estão sujeitos a exclusão de tipo. Informações sobre parâmetros de tipo não estão disponíveis em tempo de execução. Por outro lado, os arrays conhecem e podem usar informações sobre seus tipos de dados quando o programa está em execução. A tentativa de colocar um tipo inválido em uma matriz fará com que uma exceção seja lançada:

public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
Saída do console:

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Como há uma grande diferença entre arrays e genéricos, eles podem ter problemas de compatibilidade. Acima de tudo, você não pode criar um array de objetos genéricos ou mesmo apenas um array parametrizado. Isso soa um pouco confuso? Vamos dar uma olhada. Por exemplo, você não pode fazer nada disso em Java:

new List<T>[]
new List<String>[]
new T[]
Se tentarmos criar um array de List<String>objetos, obtemos um erro de compilação que reclama da criação de um array genérico:

import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       // Compilation error! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
Mas por que isso é feito? Por que a criação de tais matrizes não é permitida? Isso tudo é para fornecer segurança de tipo. Se o compilador nos permitisse criar tais arrays de objetos genéricos, poderíamos criar uma tonelada de problemas para nós mesmos. Aqui está um exemplo simples do livro "Effective Java" de Joshua Bloch:

public static void main(String[] args) {

   List<String>[] stringLists = new List<String>[1];  //  (1)
   List<Integer> intList = Arrays.asList(42, 65, 44);  //  (2)
   Object[] objects = stringLists;  //  (3)
   objects[0] = intList;  //  (4)
   String s = stringLists[0].get(0);  //  (5)
}
Vamos imaginar que criar um array like List<String>[] stringListsé permitido e não vai gerar erro de compilação. Se isso fosse verdade, aqui estão algumas coisas que poderíamos fazer: Na linha 1, criamos um array de listas: List<String>[] stringLists. Nosso array contém um arquivo List<String>. Na linha 2, criamos uma lista de números: List<Integer>. Na linha 3, atribuímos our List<String>[]a uma Object[] objectsvariável. A linguagem Java permite isso: um array de Xobjetos pode armazenar Xobjetos e objetos de todas as subclasses X. Conseqüentemente, você pode colocar qualquer coisa em uma Objectmatriz. Na linha 4, substituímos o único elemento do objects()array (a List<String>) por a List<Integer>. Assim, colocamos a List<Integer>em um array que tinha como objetivo apenas armazenarList<String>objetos! Encontraremos um erro apenas quando executarmos a linha 5. A ClassCastExceptionserá lançado em tempo de execução. Assim, uma proibição de criação de tais arrays foi adicionada ao Java. Isso nos permite evitar tais situações.

Como posso contornar o apagamento de tipo?

Bem, aprendemos sobre o apagamento de tipos. Vamos tentar enganar o sistema! :) Tarefa: Temos uma TestClass<T>classe genérica. Queremos escrever um createNewT()método para esta classe que criará e retornará um novo Tobjeto. Mas isso é impossível, certo? Todas as informações sobre o Ttipo são apagadas durante a compilação e, em tempo de execução, não podemos determinar que tipo de objeto precisamos criar. Na verdade, existe uma maneira complicada de fazer isso. Você provavelmente se lembra que Java tem uma Classclasse. Podemos usá-lo para determinar a classe de qualquer um de nossos objetos:

public class Main2 {

   public static void main(String[] args) {

       Class classInt = Integer.class;
       Class classString = String.class;

       System.out.println(classInt);
       System.out.println(classString);
   }
}
Saída do console:

class java.lang.Integer 
class java.lang.String
Mas aqui está um aspecto sobre o qual não falamos. Na documentação do Oracle, você verá que a classe Class é genérica! Tipo apagamento - 3

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

A documentação diz: "T - o tipo da classe modelada por este objeto Class". Traduzindo isso da linguagem da documentação para a fala simples, entendemos que a classe do Integer.classobjeto não é apenas Class, mas sim Class<Integer>. O tipo do String.classobjeto não é apenas Class, mas sim Class<String>, etc. Se ainda não estiver claro, tente adicionar um parâmetro de tipo ao exemplo anterior:

public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       // Compilation error!
       Class<String> classInt2 = Integer.class;
      
      
       Class<String> classString = String.class;
       // Compilation error!
       Class<Double> classString2 = String.class;
   }
}
E agora, usando esse conhecimento, podemos ignorar o apagamento de tipo e realizar nossa tarefa! Vamos tentar obter informações sobre um parâmetro de tipo. Nosso argumento de tipo será MySecretClass:

public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
E aqui está como usamos nossa solução na prática:

public class TestClass<T> {

   Class<T> typeParameterClass;

   public TestClass(Class<T> typeParameterClass) {
       this.typeParameterClass = typeParameterClass;
   }

   public T createNewT() throws IllegalAccessException, InstantiationException {
       T t = typeParameterClass.newInstance();
       return t;
   }

   public static void main(String[] args) throws InstantiationException, IllegalAccessException {

       TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
       MySecretClass secret = testString.createNewT();

   }
}
Saída do console:

A MySecretClass object was created successfully!
Acabamos de passar o argumento de classe necessário para o construtor de nossa classe genérica:

TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Isso nos permitiu salvar as informações sobre o argumento de tipo, evitando que ele fosse totalmente apagado. Como resultado, conseguimos criar umTobjeto! :) Com isso, a aula de hoje chega ao fim. Você deve sempre se lembrar do apagamento de tipo ao trabalhar com genéricos. Essa solução alternativa não parece muito conveniente, mas você deve entender que os genéricos não faziam parte da linguagem Java quando ela foi criada. Esse recurso, que nos ajuda a criar coleções parametrizadas e detectar erros durante a compilação, foi adicionado posteriormente. Em algumas outras linguagens que incluíram genéricos desde a primeira versão, não há apagamento de tipo (por exemplo, em C#). A propósito, ainda não terminamos de estudar os genéricos! Na próxima lição, você conhecerá mais alguns recursos dos genéricos. Por enquanto, seria bom resolver algumas tarefas! :)