Eu li sobre apagamento de tipo do Java no site da Oracle .
Quando ocorre o apagamento do tipo?Em tempo de compilação ou tempo de execução? Quando a turma é carregada? Quando a aula é instanciada?
Muitos sites (incluindo o tutorial oficial mencionado acima) dizem que o apagamento do tipo ocorre no momento da compilação. Se as informações de tipo forem completamente removidas no momento da compilação, como o JDK verifica a compatibilidade de tipos quando um método que usa genéricos é chamado sem informações de tipo ou informações de tipo incorretas?
Considere o seguinte exemplo: Say class A
possui um método empty(Box<? extends Number> b)
,. Nós compilamos A.java
e obtemos o arquivo de classe A.class
.
public class A {
public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}
Agora vamos criar uma outra classe B
que chama o método empty
com um argumento não-parametrizado (tipo raw): empty(new Box())
. Se compilar B.java
com A.class
no classpath, javac é inteligente o suficiente para levantar um aviso. Então, A.class
tem algumas informações de tipo armazenadas nele.
public class B {
public static void invoke() {
// java: unchecked method invocation:
// method empty in class A is applied to given types
// required: Box<? extends java.lang.Number>
// found: Box
// java: unchecked conversion
// required: Box<? extends java.lang.Number>
// found: Box
A.empty(new Box());
}
}
Meu palpite seria que o apagamento de tipo ocorre quando a classe é carregada, mas é apenas um palpite. Então, quando isso acontece?
fonte
Respostas:
O apagamento do tipo se aplica ao uso de genéricos. Definitivamente, existem metadados no arquivo de classe 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 compilação e em tempo de execução. Portanto, este código:
é compilado em
No momento da execução, não há como descobrir isso
T=String
para o objeto de lista - essas informações se foram.... mas a
List<T>
própria interface ainda se anuncia como genérica.EDIT: Apenas para esclarecer, o compilador retém as informações sobre a variável ser uma
List<String>
- mas você ainda não pode descobrir issoT=String
para o próprio objeto de lista.fonte
List<? extends InputStream>
como pode saber que tipo era quando foi criado? Mesmo se você puder descobrir o tipo de campo em que a referência foi armazenada, por que deveria? Por que você deve conseguir obter o restante das informações sobre o objeto no tempo de execução, mas não seus argumentos de tipo genérico? Parece que você está tentando fazer com que o apagamento de tipo seja algo minúsculo que não afeta os desenvolvedores - embora eu ache que seja um problema muito significativo em alguns casos.O compilador é responsável por entender os genéricos em tempo de compilação. O compilador também é responsável por jogar fora esse "entendimento" de classes genéricas, em um processo que chamamos de apagamento de tipo . Tudo acontece no momento da compilação.
Nota: Ao contrário das crenças da maioria dos desenvolvedores de Java, é possível manter informações do tipo em tempo de compilação e recuperar essas informações em tempo de execução, apesar de de maneira muito restrita. Em outras palavras: Java fornece genéricos reificados de uma maneira muito restrita .
Em relação ao apagamento do tipo
Observe que, no tempo de compilação, o compilador possui informações completas sobre o tipo, mas essas informações são descartadas intencionalmente em geral quando o código de byte é gerado, em um processo conhecido como apagamento de tipo . Isso é feito dessa maneira devido a problemas de compatibilidade: A intenção dos designers de idiomas era fornecer compatibilidade total com o código-fonte e compatibilidade completa com o código de bytes entre as versões da plataforma. Se ele fosse implementado de maneira diferente, você teria que recompilar seus aplicativos herdados ao migrar para versões mais recentes da plataforma. Da maneira como foi feito, todas as assinaturas de método são preservadas (compatibilidade com o código-fonte) e você não precisa recompilar nada (compatibilidade binária).
Sobre genéricos reificados em Java
Se você precisar manter informações do tipo em tempo de compilação, precisará empregar classes anônimas. O ponto é: no caso muito especial de classes anônimas, é possível recuperar informações completas do tipo tempo de compilação em tempo de execução, o que, em outras palavras, significa: genéricos reificados. Isso significa que o compilador não descarta informações de tipo quando classes anônimas estão envolvidas; essas informações são mantidas no código binário gerado e o sistema de tempo de execução permite recuperar essas informações.
Eu escrevi um artigo sobre este assunto:
https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/
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ê possui uma base de código compartilhada ou planeja liberar seu código ao público, não recomendo a técnica acima. Por outro lado, se você é o único usuário do seu código, pode tirar proveito do poder que essa técnica oferece a você.
Código de amostra
O artigo acima possui links para código de exemplo.
fonte
new Box<String>() {};
não no caso de acesso indireto,void foo(T) {...new Box<T>() {};...}
porque o compilador não mantém as informações de tipo para a declaração do método anexo.Se você possui um campo que é um tipo genérico, seus parâmetros de tipo são compilados na classe.
Se você tiver um método que aceita ou retorna um tipo genérico, esses parâmetros de tipo são compilados na classe
Essas informações são usadas pelo compilador para informar que você não pode passar um
Box<String>
para oempty(Box<T extends Number>)
métodoA API é complicado, mas você pode inspecionar estas informações de tipo através da API reflexão com métodos como
getGenericParameterTypes
,getGenericReturnType
e, para os campos,getGenericType
.Se você possui um código que usa um tipo genérico, o compilador insere as transmissões conforme necessário (no chamador) para verificar os tipos. Os próprios objetos genéricos são apenas do tipo bruto; o tipo parametrizado é "apagado". Portanto, quando você cria um
new Box<Integer>()
, não há informações sobre aInteger
classe noBox
objeto.A FAQ de Angelika Langer é a melhor referência que eu já vi sobre Java Generics.
fonte
Os genéricos na linguagem Java são realmente um bom guia sobre esse tópico.
Então, é no momento da compilação. A JVM nunca saberá qual
ArrayList
você usou.Eu também recomendaria a resposta do Sr. Skeet sobre Qual é o conceito de apagamento em genéricos em Java?
fonte
O apagamento do tipo ocorre no momento da compilação. O que significa apagamento de tipo é que ele esquecerá o tipo genérico, não todo tipo. Além disso, ainda haverá metadados sobre os tipos que são genéricos. Por exemplo
é convertido para
em tempo de compilação. Você pode receber avisos não porque o compilador sabe sobre de que tipo é genérico, mas pelo contrário, porque não sabe o suficiente para não garantir a segurança do tipo.
Além disso, o compilador retém as informações de tipo sobre os parâmetros em uma chamada de método, que você pode recuperar por meio de reflexão.
Este guia é o melhor que encontrei sobre o assunto.
fonte
O termo "apagamento de tipo" não é realmente a descrição correta do problema de Java com genéricos. O apagamento de tipo 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 repugnar, lembre-se da definição correta de apagamento de tipo do Wiki
O que é apagamento de tipo?
Eliminação de tipo significa jogar fora as marcas de tipo criadas em tempo de design ou as marcas de tipo inferidas em tempo de compilação, para que o programa compilado no código binário não contenha nenhuma marca de tipo. E esse é o caso de todas as linguagens de programação que compilam em 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 descriptografada (a linguagem binária permite apenas bits), pois os tipos são apenas abstrações e afirmam uma estrutura para seus valores e a semântica apropriada para lidar com eles.
Então isso é em troca, uma coisa natural normal.
O problema do Java é diferente e é causado pela maneira como ele reifica.
As declarações feitas frequentemente sobre Java não possuem genéricos reificados também estão erradas.
O Java reifica, mas de maneira errada devido à compatibilidade com versões anteriores.
O que é reificação?
Do nosso Wiki
Reificação significa converter algo abstrato (Tipo Paramétrico) em algo concreto (Tipo Concreto) por especialização.
Ilustramos isso com um exemplo simples:
Um ArrayList com definição:
é uma abstração, em detalhes um construtor de tipos, que é "reificado" quando especializado em um tipo concreto, digamos Integer:
onde
ArrayList<Integer>
é realmente um tipo.Mas é exatamente isso que o Java não faz !!! , em vez disso, eles reificam constantemente tipos abstratos com seus limites, ou seja, produzem o mesmo tipo concreto, independentemente dos parâmetros passados para a especialização:
que aqui é reificado com o objeto vinculado implícito (
ArrayList<T extends Object>
==ArrayList<T>
).Apesar disso, torna inutilizáveis as matrizes genéricas e causa alguns erros estranhos para os tipos brutos:
causa muitas ambiguidades como
consulte a mesma função:
e, portanto, a sobrecarga genérica de método não pode ser usada em Java.
fonte
Eu encontrei com o tipo de apagamento no Android. Na produção, usamos gradle com a opção minify. Após a minificação, tenho uma exceção fatal. Eu criei uma função simples para mostrar a cadeia de herança do meu objeto:
E há dois resultados dessa função:
Código não minificado:
Código reduzido:
Portanto, no código minificado, as classes parametrizadas reais são substituídas por tipos de classes brutas sem nenhuma informação de tipo. Como solução para o meu projeto, removi todas as chamadas de reflexão e as substitui por tipos de params explícitos passados em argumentos de função.
fonte