Eliminação do tipo genérico Java: quando e o que acontece?

Eu li sobre o apagamento de tipo do Java no site da Oracle .

Quando o tipo de exclusão ocorre? Em tempo de compilation ou tempo de execução? Quando a aula é carregada? Quando a aula é instanciada?

Muitos sites (incluindo o tutorial oficial mencionado acima) dizem que o tipo de eliminação ocorre em tempo de compilation. Se as informações de tipo forem completamente removidas em tempo de compilation, como a compatibilidade do tipo de verificação do JDK quando um método usando genéricos é invocado sem informações de tipo ou informações de tipo incorretas?

Considere o seguinte exemplo: Digamos que a class A tenha um método empty(Box b) . Nós compilamos A.java e obtemos o arquivo de class A.class .

 public class A { public static void empty(Box b) {} } 
 public class Box {} 

Agora criamos outra class B que invoca o método empty com um argumento não parametrizado (tipo bruto): empty(new Box()) . Se nós compilamos B.java com A.class no classpath, o javac é esperto o suficiente para criar um aviso. Então A.class tem algum tipo de informação armazenada nele.

 public class B { public static void invoke() { // java: unchecked method invocation: // method empty in class A is applied to given types // required: Box // found: Box // java: unchecked conversion // required: Box // found: Box A.empty(new Box()); } } 

Meu palpite seria que tipo de apagamento ocorre quando a class é carregada, mas é apenas um palpite. Então, quando isso acontece?

O apagamento de tipo se aplica ao uso de genéricos. Definitivamente, há metadados no arquivo de class para dizer se um método / tipo é genérico e quais são as restrições etc. Mas quando os genéricos são usados , eles são convertidos em verificações em tempo de compilation e conversões em tempo de execução. Então esse código:

 List list = new ArrayList(); list.add("Hi"); String x = list.get(0); 

é compilado em

 List list = new ArrayList(); list.add("Hi"); String x = (String) list.get(0); 

No tempo de execução, não há como descobrir que T=String para o object de lista – essa informação desapareceu.

… mas a interface List ainda se anuncia como genérica.

EDIT: Só para esclarecer, o compilador mantém as informações sobre a variável sendo uma List – mas você ainda não consegue descobrir que T=String para o object de lista em si.

O compilador é responsável por entender os genéricos em tempo de compilation. O compilador também é responsável por jogar fora essa “compreensão” das classs genéricas, em um processo que chamamos de eliminação de tipos . Tudo acontece em tempo de compilation.

Nota: Ao contrário das crenças da maioria dos desenvolvedores Java, é possível manter informações do tipo em tempo de compilation e recuperar essas informações em tempo de execução, apesar de uma maneira muito restrita. Em outras palavras: o Java fornece genéricos reificados de maneira muito restrita .

Quanto ao tipo de apagamento

Observe que, em tempo de compilation, o compilador tem informações de tipo completo disponíveis, mas essa informação é intencionalmente descartada em geral quando o código de byte é gerado, em um processo conhecido como eliminação de tipo . Isso é feito dessa maneira devido a problemas de compatibilidade: A intenção dos designers de linguagem era fornecer compatibilidade total de código-fonte e compatibilidade total de código de byte entre as versões da plataforma. Se fosse implementado de forma diferente, você teria que recompilar seus aplicativos legados ao migrar para versões mais novas da plataforma. Do jeito que foi feito, todas as assinaturas de methods são preservadas (compatibilidade com código-fonte) e você não precisa recompilar nada (compatibilidade binária).

Em relação aos genéricos reificados em Java

Se você precisar manter informações do tipo em tempo de compilation, precisará empregar classs anônimas. O ponto é: no caso muito especial de classs anônimas, é possível recuperar informações completas do tipo em tempo de compilation em tempo de execução, o que, em outras palavras, significa: genéricos reificados. Isso significa que o compilador não elimina informações de tipo quando classs anônimas são envolvidas; essas informações são mantidas no código binário gerado e o sistema de tempo de execução permite que você recupere essas informações.

Eu escrevi um artigo sobre este assunto:

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

Uma observação sobre a técnica descrita no artigo acima é que a técnica é obscura para a maioria dos desenvolvedores. Apesar de funcionar e funcionar bem, a maioria dos desenvolvedores se sente confusa ou desconfortável com a técnica. Se você tiver uma base de código compartilhado ou planeja liberar seu código para o público, não recomendo a técnica acima. Por outro lado, se você for o único usuário de seu código, poderá aproveitar o poder que esta técnica lhe oferece.

Código de amostra

O artigo acima possui links para código de exemplo.

Se você tiver um campo que é um tipo genérico, seus parâmetros de tipo serão compilados na class.

Se você tiver um método que obtenha ou retorne um tipo genérico, esses parâmetros de tipo serão compilados na class.

Esta informação é o que o compilador usa para lhe dizer que você não pode passar um Box para o método empty(Box) .

A API é complicada, mas você pode inspecionar essas informações de tipo por meio da API de reflection com methods como getGenericParameterTypes , getGenericReturnType e, para campos, getGenericType .

Se você tiver código que usa um tipo genérico, o compilador inserirá conversões conforme necessário (no chamador) para verificar os tipos. Os objects genéricos em si são apenas o tipo bruto; o tipo parametrizado é “apagado”. Portanto, quando você cria um new Box() , não há informações sobre a class Integer no object Box .

A FAQ de Angelika Langer é a melhor referência que vi para Java Generics.

Generics in Java Language é um bom guia sobre este tópico.

Os genéricos são implementados pelo compilador Java como uma conversão de front end chamada de apagamento. Você pode (quase) pensar nisso como uma tradução de fonte para fonte, em que a versão genérica de loophole() é convertida para a versão não genérica.

Então, é em tempo de compilation. A JVM nunca saberá qual ArrayList você usou.

Eu também recomendo a resposta do Sr. Skeet sobre Qual é o conceito de apagamento em genéricos em Java?

O apagamento de tipo ocorre em tempo de compilation. O tipo de apagamento significa que ele se esquecerá do tipo genérico, não de todo tipo. Além disso, ainda haverá metadados sobre os tipos sendo genéricos. Por exemplo

 Box b = new Box(); String x = b.getDefault(); 

é convertido para

 Box b = new Box(); String x = (String) b.getDefault(); 

em tempo de compilation. Você pode receber avisos não porque o compilador sabe de qual tipo é o genérico, mas, pelo contrário, porque não sabe o suficiente, por isso não pode garantir a segurança do tipo.

Além disso, o compilador mantém as informações de tipo sobre os parâmetros em uma chamada de método, que você pode recuperar por meio de reflection.

Este guia é o melhor que encontrei sobre o assunto.

Eu encontrei com o apagamento de tipo no Android. Na produção, usamos gradle com a opção minify. Após a minificação, recebi uma exceção fatal. Eu fiz uma function simples para mostrar a cadeia de inheritance do meu object:

 public static void printSuperclasss(Class clazz) { Type superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); while (superClass != null && clazz != null) { clazz = clazz.getSuperclass(); superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); } } 

E há dois resultados dessa function:

Código não minificado:

 D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: android.support.v7.util.SortedList$Callback D/Reflection: this class: android.support.v7.util.SortedList$Callback D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

Código Minificado:

 D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: class com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: class android.support.v7.ge D/Reflection: this class: android.support.v7.ge D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

Assim, no código minificado, classs realçadas parametrizadas são substituídas por tipos de classs brutos, sem qualquer tipo de informação. Como solução para o meu projeto, removi todas as chamadas de reflection e as substituímos por tipos de parâmetros explícitos transmitidos em argumentos de function.

O termo “tipo de apagamento” não é realmente a descrição correta do problema de Java com genéricos. O apagamento de tipos não é, por si só, uma coisa ruim, na verdade, é muito necessário para o desempenho e é frequentemente usado em várias linguagens como C ++, Haskell, D.

Antes de você repugnar, por favor, lembre-se da definição correta de eliminação de tipos do Wiki

O que é tipo de apagamento?

tipo de eliminação refere-se ao processo de tempo de carregamento pelo qual as annotations de tipo explícito são removidas de um programa, antes de ser executado em tempo de execução

Eliminar o tipo significa eliminar tags de tipo criadas em tempo de design ou tags de tipo inferidas em tempo de compilation, de forma que o programa compilado em código binário não contenha tags de tipo. E esse é o caso de toda linguagem de programação compilada para código binário, exceto em alguns casos em que você precisa de tags de tempo de execução. Essas exceções incluem, por exemplo, todos os tipos existenciais (tipos de referência Java que são subtipáveis, qualquer tipo em vários idiomas, tipos de união). A razão para o apagamento de tipos é que os programas são transformados em uma linguagem que é de algum tipo unidipo (linguagem binária que permite apenas bits), pois os tipos são apenas abstrações e afirmam uma estrutura para seus valores e a semântica apropriada para manipulá-los.

Então isso é em troca, uma coisa natural normal.

O problema do Java é diferente e causado pelo modo como ele é reificado.

As declarações feitas com frequência sobre Java não têm genéricos reificados também estão erradas.

Java reifica, mas de maneira errada devido à compatibilidade com versões anteriores.

O que é reificação?

Do nosso Wiki

Reificação é o processo pelo qual uma idéia abstrata sobre um programa de computador é transformada em um modelo de dados explícito ou outro object criado em uma linguagem de programação.

Reificação significa converter algo abstrato (Tipo Paramétrico) em algo concreto (Tipo Concreto) por especialização.

Ilustramos isso com um exemplo simples:

Uma ArrayList com definição:

 ArrayList { T[] elems; ...//methods } 

é uma abstração, em detalhes, um construtor de tipo, que é “reificado” quando especializado em um tipo concreto, digamos Inteiro:

 ArrayList { Integer[] elems; } 

onde ArrayList é realmente um tipo.

Mas isso é exatamente o que o Java não faz !!! ao invés disso, eles reificam constantemente tipos abstratos com seus limites, ou seja, produzindo o mesmo tipo de concreto independente dos parâmetros passados ​​para especialização:

 ArrayList { Object[] elems; } 

que é aqui reificado com o object ligado implícito ( ArrayList == ArrayList ).

Apesar disso, torna as matrizes genéricas inutilizáveis ​​e causam alguns erros estranhos para tipos brutos:

 List l= List.of("h","s"); List lRaw=l l.add(new Object()) String s=l.get(2) //Cast Exception 

causa muitas ambiguidades como

 void function(ArrayList list){} void function(ArrayList list){} void function(ArrayList list){} 

consulte a mesma function:

 void function(ArrayList list) 

e, portanto, a sobrecarga de método genérico não pode ser usada em Java.