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! Na ú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.
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
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 List
e 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 Cat
objetos. Essas informações são apagadas durante a compilação — apenas o fato de você ter uma List<Object> cats
lista 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 TestClass
classe genérica. É bem simples: na verdade é uma pequena "coleção" de 2 objetos, que são armazenados imediatamente quando o objeto é criado. Tem 2 T
campos. Quando o createAndAdd2Values()
método é executado, os dois objetos passados ( Object a
e Object b
devem ser convertidos para o T
tipo e depois adicionados ao TestClass
objeto. No main()
método, criamos um TestClass<Integer>
, ou seja, o Integer
argumento de tipo substitui o Integer
parâmetro de tipo. Também estamos passando a Double
e a String
para o createAndAdd2Values()
método. Você acha que nosso programa funcionará? Afinal, especificamos Integer
como o argumento de tipo, mas String
definitivamente 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 Integer
argumento de tipo usado para instanciar nosso TestClass<Integer> test
objeto foram apagadas quando o código foi compilado. O campo se torna TestClass<Object> test
. Nossos argumentos Double
e String
foram facilmente convertidos em Object
objetos (eles não são convertidos em Integer
objetos 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
, Integer
e nossa própria Cat
classe. 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 (comoList
). 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[] objects
variável. A linguagem Java permite isso: um array de X
objetos pode armazenar X
objetos e objetos de todas as subclasses X
. Conseqüentemente, você pode colocar qualquer coisa em uma Object
matriz. 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 ClassCastException
será 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 umaTestClass<T>
classe genérica. Queremos escrever um createNewT()
método para esta classe que criará e retornará um novo T
objeto. Mas isso é impossível, certo? Todas as informações sobre o T
tipo 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 Class
classe. 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!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
objeto não é apenas Class
, mas sim Class<Integer>
. O tipo do String.class
objeto 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 umT
objeto! :) 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! :)
GO TO FULL VERSION