Apagamento do tipo genérico Java: quando e o que acontece?

238

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 Apossui um método empty(Box<? extends Number> b),. Nós compilamos A.javae 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 Bque chama o método emptycom um argumento não-parametrizado (tipo raw): empty(new Box()). Se compilar B.javacom A.classno 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?

Radiodef
fonte
2
Uma versão mais "genérica" ​​desta pergunta: stackoverflow.com/questions/313584/…
Ciro Santilli #: 26315
@afryingpan: O artigo mencionado na minha resposta explica em detalhes como e quando o apagamento do tipo acontece. Também explica quando as informações de tipo são mantidas. Em outras palavras: genéricos reificados estão disponíveis em Java, ao contrário da crença generalizada. Veja: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Respostas:

240

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:

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

é compilado em

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

No momento da execução, não há como descobrir isso T=Stringpara 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 isso T=Stringpara o próprio objeto de lista.

Jon Skeet
fonte
6
Não, mesmo no uso de um tipo genérico, pode haver metadados disponíveis no tempo de execução. Uma variável local não é acessível por meio do Reflection, mas para um parâmetro de método declarado como "Lista <> l", haverá metadados de tipo em tempo de execução, disponíveis na API do Reflection. Yep, "Tipo de apagamento" não é tão simples como muitas pessoas pensam ...
Rogério
4
@ Rogerio: Como eu respondi ao seu comentário, acredito que você está ficando confuso entre poder obter o tipo de uma variável e ser capaz de obter o tipo de um objeto . O objeto em si não conhece seu argumento de tipo, mesmo que o campo conheça.
31909 Jon Skeet
Obviamente, apenas olhando para o objeto em si, você não pode saber que é uma Lista <>. Mas os objetos não aparecem do nada. Eles são criados localmente, transmitidos como um argumento de chamada de método, retornados como o valor de retorno de uma chamada de método ou lidos de um campo de algum objeto ... Em todos esses casos, você PODE saber em tempo de execução qual é o tipo genérico. implicitamente ou usando a API Java Reflection.
Rogério
13
@ Rogerio: Como você sabe de onde o objeto veio? Se você possui um parâmetro do tipo, 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.
31909 Jon Skeet
Mas o apagamento de tipo é uma coisa minúscula que realmente não afeta os desenvolvedores! É claro que não posso falar pelos outros, mas, na MINHA experiência, nunca foi grande coisa. Na verdade, aproveito as informações do tipo de tempo de execução no design da minha API de simulação Java (JMockit); Ironicamente, as APIs de simulação do .NET parecem tirar menos vantagem do sistema de tipos genéricos disponível em C #.
Rogério
99

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.

Richard Gomes
fonte
1
@ will824: melhorei enormemente a resposta e adicionei um link para alguns casos de teste. Cheers :)
Richard Gomes
1
Na verdade, eles não mantêm a compatibilidade binária e de origem: oracle.com/technetwork/java/javase/compatibility-137462.html Onde posso ler mais sobre a intenção deles? Os documentos dizem que ele usa apagamento de tipo, mas não diz o porquê.
Dzmitry Lazerka
@ Richard De fato, excelente artigo! Você pode adicionar que as classes locais também funcionam e que, em ambos os casos (classes anônimas e locais), as informações sobre o argumento de tipo desejado são mantidas apenas no caso de acesso direto e 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.
Yann-Gaël Guéhéneuc
Corrigi um link quebrado para o meu artigo. Estou lentamente pesquisando minha vida e recuperando meus dados. :-)
Richard Gomes
33

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étodo

A API é complicado, mas você pode inspecionar estas informações de tipo através da API reflexão com métodos como getGenericParameterTypes, getGenericReturnTypee, 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 a Integerclasse no Boxobjeto.

A FAQ de Angelika Langer é a melhor referência que eu já vi sobre Java Generics.

erickson
fonte
2
Na verdade, é o tipo genérico formal dos campos e métodos que são compilados na classe, ou seja, geralmente "T". Para obter o tipo real do tipo genérico, você deve usar o "truque de classe anônima" .
Yann-Gaël Guéhéneuc
13

Os genéricos na linguagem Java são realmente um bom guia sobre esse tópico.

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

Então, é no momento da compilação. A JVM nunca saberá qual ArrayListvocê usou.

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

Eugene Yokota
fonte
6

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

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

é convertido para

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

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.

Vinko Vrsalovic
fonte
6

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?

apagamento de tipo refere-se ao processo de tempo de carregamento pelo qual as anotações de tipo explícitas são removidas de um programa, antes de serem executadas no tempo de execução

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 é o processo pelo qual uma idéia abstrata sobre um programa de computador é transformada em um modelo de dados explícito ou outro objeto 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:

Um ArrayList com definição:

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

é uma abstração, em detalhes um construtor de tipos, que é "reificado" quando especializado em um tipo concreto, digamos Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

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:

ArrayList
{
    Object[] elems;
}

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:

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

causa muitas ambiguidades como

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

consulte a mesma função:

void function(ArrayList list)

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

iconfly
fonte
2

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:

public static void printSuperclasses(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 função:

Código não minificado:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

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 reduzido:

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.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

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.

porfirion
fonte