Qual é o conceito de apagamento em genéricos em Java?
É 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 aList<String>
) por exemplo. Em Java, isso não é viável - você pode descobrir que é umArrayList
, 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 (asClass<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:
O
-printflat
sinalizador é entregue ao compilador que gera os arquivos. (A-XD
parte é o que dizjavac
para entregá-lo ao jar executável que realmente faz a compilação, e não apenasjavac
, 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 parafor
loops regulares etc. É bom ver as pequenas coisas que estão acontecendo automaticamente.fonte
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.
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.
fonte
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:
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 imediatoClassCastException
.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) "
fonte
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.
fonte
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.
fonte
Existem boas explicações. Eu apenas adiciono um exemplo para mostrar como o apagamento de tipo funciona com um descompilador.
Classe original,
Código descompilado de seu bytecode,
fonte
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>
eList<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 deasList
éObject
, porqueT
não tem limite.T
na definição demax
éComparable
, porqueT
vinculadoComparable<? super T>
.T
na definição final demax
éObject
, porqueT
limitouObject
&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.
Pretendemos que este código funcione da seguinte maneira:
No entanto, neste caso, os apagamentos das assinaturas dos dois métodos são idênticos:
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 :)
fonte