Qual é o conceito de apagamento em genéricos em Java?

141

Qual é o conceito de apagamento em genéricos em Java?

Tushu
fonte

Respostas:

200

É 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.Objectonde 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:

Jon Skeet
fonte
6
@ Rogerio: Não, os objetos não terão tipos genéricos diferentes. Os campos conhecem os tipos, mas os objetos não.
31909 Jon Skeet
8
@ Rogerio: Absolutamente - é extremamente fácil descobrir em tempo de execução se algo que é fornecido apenas como 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>).
31909 Jon Skeet
6
Eu nunca afirmei que era sempre ou quase sempre um problema - mas é pelo menos razoavelmente frequente um problema na minha experiência. Existem vários lugares em que sou forçado a adicionar um 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.allOfpor 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)
Jon Skeet
5
Antes de usar os genéricos do .NET, achei os genéricos do Java desajeitados de várias maneiras (e o curinga ainda é uma dor de cabeça, embora a forma de variação "especificada pelo chamador" definitivamente tenha vantagens) - mas foi somente depois que eu usei os genéricos do .NET por um tempo, vi quantos padrões se tornaram estranhos ou impossíveis com os genéricos Java. É o paradoxo de Blub novamente. Não estou dizendo que os genéricos .NET também não tenham desvantagens - existem vários tipos de relacionamentos que não podem ser expressados, infelizmente - mas prefiro-os aos Java genéricos.
31909 Jon Skeet
5
@ Rogerio: Há muita coisa que você pode fazer com reflexão - mas eu não costumo achar que quero fazer essas coisas quase tão frequentemente quanto as coisas que não posso fazer com os genéricos Java. Eu não quero descobrir o argumento de tipo para um campo quase tantas vezes quanto eu quero descobrir o argumento de tipo de um objeto real.
Jon Skeet
41

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 -printflatsinalizador é entregue ao compilador que gera os arquivos. (A -XDparte é o que diz javacpara 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 forloops no estilo foreach são expandidos para forloops regulares etc. É bom ver as pequenas coisas que estão acontecendo automaticamente.

jigawot
fonte
29

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();

    }
} 
Parag
fonte
Tentei usar o descompilador java para ver o código após o apagamento do tipo .class, mas o arquivo .class ainda possui informações de tipo. Eu tentei jigawotdisse, funciona.
Frank
25

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:

  • Compatibilidade de origem (É bom ter ...)
  • Compatibilidade binária (deve ter!)
  • Compatibilidade de migração
    • Os programas existentes devem continuar funcionando
    • Bibliotecas existentes devem poder usar tipos genéricos
    • Deve ter!

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 checkCollectionmé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:

  • quando uma coleção é passada para uma biblioteca de terceiros e é imperativo que o código da biblioteca não corrompa a coleção inserindo um elemento do tipo errado.
  • um programa falha com a 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.Iteratorclasse 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();
}
VonC
fonte
2
Observe que a compatibilidade com versões anteriores poderia ter sido alcançada sem o apagamento do tipo, mas não sem que os programadores Java aprendessem um novo conjunto de coleções. Essa é exatamente a rota que o .NET seguiu. Em outras palavras, é essa terceira bala que é a mais importante. (Continuação.)
Jon Skeet
15
Pessoalmente, acho que foi um erro míope - deu uma vantagem a curto prazo e uma desvantagem a longo prazo.
Jon Skeet
8

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.

Daniel Spiewak
fonte
6
O inconveniente não é quando você deseja fazer coisas "impertinentes" no momento da execução. É quando você deseja fazer coisas perfeitamente razoáveis ​​no momento da execução. De fato, o apagamento de tipo permite que você faça coisas muito menos importantes - como converter uma Lista <> na Lista e, em seguida, na Lista <Data> com apenas avisos.
Jon Skeet
5

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.

Andrew Kennan
fonte
3
Os genéricos Java não podem representar tipos de valor de qualquer maneira - não existe uma List <int>. No entanto, não há nenhuma passagem por referência em Java em tudo - é estritamente passagem por valor (onde esse valor pode ser uma referência.)
Jon Skeet
2

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);


   }
}
snr
fonte
2

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:

  • Verificações de tipo mais fortes em tempo de compilação.
  • Eliminação de elencos.
  • Habilitando os programadores a implementar algoritmos genéricos.

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:

  • Substitua todos os parâmetros de tipo em tipos genéricos por seus limites ou Objeto se os parâmetros de tipo não tiverem limites. O bytecode produzido, portanto, contém apenas classes, interfaces e métodos comuns.
  • Insira moldes do tipo, se necessário, para preservar a segurança do tipo.
  • Gere métodos de ponte para preservar o polimorfismo em tipos genéricos estendidos.

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

  • O apagamento de List<Integer>, List<String>e List<List<String>>é List.
  • O apagamento de List<Integer>[]é List[].
  • O apagamento de Listsi é, da mesma forma para qualquer tipo bruto.
  • O apagamento de int é ele próprio, da mesma forma para qualquer tipo primitivo.
  • A eliminação de Integeré ela mesma, da mesma forma para qualquer tipo sem parâmetros de tipo.
  • O apagamento de Tna definição de asListé Object, porque T não tem limite.
  • O apagamento de Tna definição de maxé Comparable, porque T vinculado Comparable<? super T>.
  • O apagamento de Tna definição final de maxé Object, porque Tlimitou 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 :)

Atif
fonte