Respostas:
É basicamente a maneira como os genéricos são implementados em Java por meio de truques do compilador. O código genérico compilado na verdade só usa java.lang.Object
onde quer que você fale T
(ou algum outro parâmetro de tipo) - e há alguns metadados para informar ao compilador que realmente é um tipo genérico.
Quando você compila algum código em um tipo ou método genérico, o compilador descobre o que você realmente quer dizer (ou seja, qual é o argumento do tipo T
) e verifica no tempo de compilação que você está fazendo a coisa certa, mas o código emitido apenas fala em termos de java.lang.Object
- o compilador gera conversões extras sempre que necessário. No tempo de execução, a List<String>
e a List<Date>
são exatamente iguais; as informações de tipo extra foram apagadas pelo compilador.
Compare isso com, digamos, C #, onde as informações são mantidas no tempo de execução, permitindo que o código contenha expressões como a typeof(T)
que é equivalente a T.class
- exceto que a última é inválida. (Existem outras diferenças entre genéricos .NET e Java genéricos, lembre-se.) O apagamento de tipo é a fonte de muitas das mensagens "estranhas" de aviso / erro ao lidar com genéricos Java.
Outros recursos:
Object
(em um cenário de tipo fraco) é realmente a List<String>
) por exemplo. Em Java, isso não é viável - você pode descobrir que é um ArrayList
, mas não o que era o tipo genérico original. Esse tipo de coisa pode surgir em situações de serialização / desserialização, por exemplo. Outro exemplo é o local em que um contêiner deve ser capaz de construir instâncias de seu tipo genérico - você deve passar esse tipo separadamente em Java (as Class<T>
).
Class<T>
parâmetro a um construtor (ou método genérico) simplesmente porque o Java não retém essas informações. Veja, EnumSet.allOf
por exemplo - o argumento de tipo genérico para o método deve ser suficiente; por que preciso especificar um argumento "normal" também? Resposta: digite apagamento. Esse tipo de coisa polui uma API. Por interesse, você já usou muito os genéricos do .NET? (continuação)
Apenas como uma observação, é um exercício interessante ver o que o compilador está fazendo quando executa o apagamento - facilita a compreensão de todo o conceito. Há um sinalizador especial de que você pode passar o compilador para gerar arquivos java que tiveram os genéricos apagados e lançados. Um exemplo:
javac -XD-printflat -d output_dir SomeFile.java
O -printflat
sinalizador é entregue ao compilador que gera os arquivos. (A -XD
parte é o que diz javac
para entregá-lo ao jar executável que realmente faz a compilação, e não apenas javac
, mas discordo ...) -d output_dir
É necessário porque o compilador precisa de algum lugar para colocar os novos arquivos .java.
Isso, é claro, faz mais do que apenas apagar; todo o material automático que o compilador faz é feito aqui. Por exemplo, construtores padrão também são inseridos, os novos for
loops no estilo foreach são expandidos para for
loops regulares etc. É bom ver as pequenas coisas que estão acontecendo automaticamente.
Apagamento significa literalmente que as informações de tipo presentes no código-fonte são apagadas do bytecode compilado. Vamos entender isso com algum código.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsErasure {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("Hello");
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
}
}
Se você compilar esse código e descompilar com um descompilador Java, obterá algo assim. Observe que o código descompilado não contém nenhum rastreamento das informações de tipo presentes no código-fonte original.
import java.io.PrintStream;
import java.util.*;
public class GenericsErasure
{
public GenericsErasure()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("Hello");
String s;
for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
s = (String)iter.next();
}
}
jigawot
disse, funciona.
Para concluir a resposta já muito completa de Jon Skeet, você precisa entender que o conceito de apagamento de tipo deriva de uma necessidade de compatibilidade com versões anteriores do Java .
Apresentada inicialmente no EclipseCon 2007 (não está mais disponível), a compatibilidade incluía os seguintes pontos:
Resposta original:
Conseqüentemente:
new ArrayList<String>() => new ArrayList()
Existem proposições para uma maior reificação . Reify sendo "Considere um conceito abstrato como real", onde construções de linguagem devem ser conceitos, não apenas açúcar sintático.
Também devo mencionar o checkCollection
método do Java 6, que retorna uma exibição segura dinamicamente tipográfica da coleção especificada. Qualquer tentativa de inserir um elemento do tipo errado resultará em um imediato ClassCastException
.
O mecanismo genérico na linguagem fornece verificação do tipo em tempo de compilação (estática), mas é possível derrotar esse mecanismo com projeções não verificadas .
Normalmente, isso não é um problema, pois o compilador emite avisos em todas essas operações não verificadas.
No entanto, há momentos em que a verificação de tipo estático por si só não é suficiente, como:
ClassCastException
, indicando que um elemento digitado incorretamente foi colocado em uma coleção parametrizada. Infelizmente, a exceção pode ocorrer a qualquer momento após a inserção do elemento incorreto, geralmente fornecendo pouca ou nenhuma informação sobre a fonte real do problema.Atualize julho de 2012, quase quatro anos depois:
Agora está detalhado (2012) em " Regras de compatibilidade de migração de API (teste de assinatura) "
A linguagem de programação Java implementa genéricos usando apagamento, o que garante que versões herdadas e genéricas geralmente gerem arquivos de classe idênticos, exceto por algumas informações auxiliares sobre os tipos. A compatibilidade binária não é interrompida porque é possível substituir um arquivo de classe herdado por um arquivo de classe genérico sem alterar ou recompilar qualquer código do cliente.
Para facilitar a interface com o código legado não genérico, também é possível usar o apagamento de um tipo parametrizado como um tipo. Esse tipo é chamado de tipo bruto ( Java Language Specification 3 / 4.8 ). Permitir o tipo bruto também garante compatibilidade com versões anteriores do código fonte.
De acordo com isso, as seguintes versões da
java.util.Iterator
classe são compatíveis com versões anteriores do código-fonte e binário:
Class java.util.Iterator as it is defined in Java SE version 1.4:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
Class java.util.Iterator as it is defined in Java SE version 5.0:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Complementando a resposta já complementada de Jon Skeet ...
Foi mencionado que a implementação de genéricos por meio de apagamento leva a algumas limitações irritantes (por exemplo, não new T[42]
). Também foi mencionado que o principal motivo para fazer as coisas dessa maneira era a compatibilidade com versões anteriores no bytecode. Isso também é verdade (principalmente). O bytecode gerado -target 1.5 é um pouco diferente de apenas a remoção do açúcar da conversão -target 1.4. Tecnicamente, é possível (através de imensos truques) obter acesso a instanciações de tipo genérico em tempo de execução , provando que realmente há algo no bytecode.
O ponto mais interessante (que não foi levantado) é que a implementação de genéricos usando apagamento oferece um pouco mais de flexibilidade no que o sistema de tipo de alto nível pode realizar. Um bom exemplo disso seria a implementação da JVM do Scala versus o CLR. Na JVM, é possível implementar tipos superiores diretamente, devido ao fato de a própria JVM não impor restrições a tipos genéricos (uma vez que esses "tipos" estão efetivamente ausentes). Isso contrasta com o CLR, que possui conhecimento em tempo de execução das instanciações de parâmetros. Por esse motivo, o próprio CLR deve ter algum conceito de como os genéricos devem ser usados, anulando as tentativas de estender o sistema com regras imprevistas. Como resultado, os tipos mais altos do Scala no CLR são implementados usando uma forma estranha de apagamento emulada no próprio compilador,
A eliminação pode ser inconveniente quando você deseja fazer coisas malcriadas em tempo de execução, mas oferece a maior flexibilidade aos escritores do compilador. Suponho que seja parte do motivo de não desaparecer tão cedo.
Pelo que entendi (sendo um cara do .NET ), a JVM não tem conceito de genéricos; portanto, o compilador substitui os parâmetros de tipo por Object e executa toda a conversão para você.
Isso significa que os genéricos Java nada mais são do que açúcar de sintaxe e não oferecem nenhuma melhoria de desempenho para tipos de valor que requerem boxe / unboxing quando passados por referência.
Existem boas explicações. Eu apenas adiciono um exemplo para mostrar como o apagamento de tipo funciona com um descompilador.
Classe original,
import java.util.ArrayList;
import java.util.List;
public class S<T> {
T obj;
S(T o) {
obj = o;
}
T getob() {
return obj;
}
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("Hello");
// for-each
for(String s : list) {
String temp = s;
System.out.println(temp);
}
// stream
list.forEach(System.out::println);
}
}
Código descompilado de seu bytecode,
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
public class S {
Object obj;
S(Object var1) {
this.obj = var1;
}
Object getob() {
return this.obj;
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("Hello");
// for-each
Iterator iterator = var1.iterator();
while (iterator.hasNext()) {
String string;
String string2 = string = (String)iterator.next();
System.out.println(string2);
}
// stream
PrintStream printStream = System.out;
Objects.requireNonNull(printStream);
var1.forEach(printStream::println);
}
}
Por que usar Generices
Em poucas palavras, os genéricos permitem que tipos (classes e interfaces) sejam parâmetros ao definir classes, interfaces e métodos. Assim como os parâmetros formais mais familiares usados nas declarações de método, os parâmetros de tipo fornecem uma maneira de reutilizar o mesmo código com entradas diferentes. A diferença é que as entradas para parâmetros formais são valores, enquanto as entradas para digitar parâmetros são tipos. O código que usa genéricos possui muitos benefícios sobre o código não genérico:
O que é apagamento de tipo
Os genéricos foram introduzidos na linguagem Java para fornecer verificações mais restritas de tipo em tempo de compilação e para oferecer suporte à programação genérica. Para implementar genéricos, o compilador Java aplica o apagamento de tipo a:
[NB] -O que é método de ponte? Em breve, no caso de uma interface parametrizada como Comparable<T>
, isso pode fazer com que métodos adicionais sejam inseridos pelo compilador; esses métodos adicionais são chamados de pontes.
Como funciona o apagamento
A exclusão de um tipo é definida da seguinte forma: descarte todos os parâmetros de tipo de tipos parametrizados e substitua qualquer variável de tipo pela exclusão de seu limite, ou por Object se ele não tiver limite, ou pelo apagamento do limite mais à esquerda, se houver. limites múltiplos. aqui estão alguns exemplos:
List<Integer>
, List<String>
e List<List<String>>
é List
.List<Integer>[]
é List[]
.List
si é, da mesma forma para qualquer tipo bruto.Integer
é ela mesma, da mesma forma para qualquer tipo sem parâmetros de tipo.T
na definição de asList
é Object
, porque T
não tem limite.T
na definição de max
é Comparable
, porque T
vinculado Comparable<? super T>
.T
na definição final de max
é Object
, porque
T
limitou Object
& Comparable<T>
e tomamos o apagamento do limite mais à esquerda.Precisa ter cuidado ao usar genéricos
Em Java, dois métodos distintos não podem ter a mesma assinatura. Como os genéricos são implementados por apagamento, também se segue que dois métodos distintos não podem ter assinaturas com o mesmo apagamento. Uma classe não pode sobrecarregar dois métodos cujas assinaturas têm o mesmo apagamento e uma classe não pode implementar duas interfaces que têm o mesmo apagamento.
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List<Integer> ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List<String> strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
Pretendemos que este código funcione da seguinte maneira:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
No entanto, neste caso, os apagamentos das assinaturas dos dois métodos são idênticos:
boolean allZero(List)
Portanto, um conflito de nome é relatado no momento da compilação. Não é possível atribuir aos dois métodos o mesmo nome e tentar distinguir entre eles sobrecarregando, porque após o apagamento é impossível distinguir uma chamada de método da outra.
Espero que o Reader goste :)