W3docs

Apagamento de Tipos em Java

Como o Java implementa generics através do apagamento de tipos, o que é removido em tempo de execução e as consequências disso.

Apagamento de tipos é como o Java implementa generics: os parâmetros de tipo existem enquanto o compilador está em execução e, em seguida, são descartados antes que o bytecode seja escrito. Um List<String> e um List<Integer> chegam à JVM como um simples List — intercambiáveis em tempo de execução. O sistema de tipos em tempo de compilação garante a segurança; o tempo de execução usa o mesmo List que a linguagem tinha em 1995. Essa escolha de design é o fato mais importante sobre os generics do Java e explica todas as restrições estranhas que você encontrará no próximo capítulo.

Este capítulo aborda o que o apagamento substitui nos parâmetros de tipo, por que o Java o escolheu em vez de generics reificados, as consequências em tempo de execução (getClass, instanceof, new T()), os métodos bridge gerados pelo compilador que mantêm o funcionamento das sobreposições, e por que o apagamento bloqueia certas sobrecargas. Se você é novo em generics, comece com Java Generics primeiro.

Por que o Java fez dessa forma

Quando os generics foram adicionados no Java 5, a biblioteca padrão já tinha uma década. Toda List, Map e Comparator existente era não-genérica, e todos os programas existentes no mundo as usavam como tipos brutos. O requisito rígido da Sun era compatibilidade binária retroativa: código compilado contra a biblioteca padrão anterior à versão 5 tinha que continuar funcionando na nova sem recompilação.

Dois designs estavam em consideração:

  1. Generics reificados — manter as informações de tipo em tempo de execução, como o C# acabou fazendo. Mais rápido, mais expressivo, mas requer que todo arquivo de classe existente no mundo seja re-emitido.
  2. Generics com apagamento — remover as informações de tipo em tempo de compilação, deixando a forma do bytecode inalterada. Mais lento por chamada (casts extras), menos expressivo (sem new T()), mas todos os JARs antigos continuam funcionando sem alterações.

A Sun escolheu o apagamento. O preço pragmático pela atualização foi pago em flexibilidade de longo prazo da linguagem. Não é o design que alguém escolheria a partir do zero — mas é o design que o Java tem, e entendê-lo faz tudo sobre generics se encaixar.

O que o apagamento realmente faz

Quando o compilador vê um tipo genérico, ele faz duas coisas:

  1. Apaga cada parâmetro de tipo para seu limite mais à esquerda — ou para Object se não houver limite.
  2. Insere casts em todo lugar onde um valor genérico é lido, para que os valores de tempo de execução cheguem nos slots corretos.

Considere esta classe genérica:

public class Box<T extends Number> {
  private T value;

  public Box(T value)        { this.value = value; }
  public T   get()            { return value; }
  public void set(T value)    { this.value = value; }
}

Após o apagamento, o bytecode se parece aproximadamente com isto (em equivalente de código-fonte Java):

public class Box {
  private Number value;

  public Box(Number value)            { this.value = value; }
  public Number get()                  { return value; }
  public void   set(Number value)      { this.value = value; }
}

O T desapareceu. Tornou-se Number porque esse era o limite. Se não houvesse limite, teria se tornado Object.

E no local da chamada:

Box<Integer> b = new Box<>(42);
int x = b.get();           // source

torna-se (após o apagamento):

Box b = new Box(42);
int x = (Integer) b.get();   // compiler inserted the cast

O cast era invisível no código-fonte. O compilador o adiciona porque sabe que o tipo no nível da fonte era Integer, mesmo que o tipo no nível do bytecode seja Number (ou Object).

O que isso significa em tempo de execução

Várias consequências decorrem diretamente do apagamento, e são a fonte de cada momento "mas eu achei que poderia…" que um desenvolvedor tem com os generics do Java:

Box<Integer> a = new Box<>(1);
Box<Double>  b = new Box<>(1.0);

a.getClass() == b.getClass();   // true — both are Box.class

Não existe Box<Integer>.class ou Box<Double>.class em tempo de execução — existe apenas Box.class. As duas instâncias são a mesma classe, porque a JVM literalmente não consegue diferenciá-las.

Você não pode perguntar "isto é um instanceof Box<Integer>":

if (obj instanceof Box<Integer>) { ... }   // ❌ does not compile
if (obj instanceof Box<?>)        { ... }   // ✓ — wildcard is allowed
if (obj instanceof Box)           { ... }   // ✓ — raw form works

Você não pode escrever new T():

public class Factory<T> {
  public T create() { return new T(); }    // ❌ — no T at runtime
}

Você não pode capturar um tipo de exceção genérico:

try { ... }
catch (MyException<String> e) { ... }      // ❌

Todos esses erros de compilação têm a mesma origem: a JVM não possui o parâmetro de tipo no momento em que esse código precisaria ser executado. O compilador se recusa a emitir código que sabe que não pode ter sucesso.

Nota
A solução padrão para "preciso criar um T" é passar o tipo explicitamente — geralmente como um parâmetro Class<T> e clazz.getDeclaredConstructor().newInstance(), ou uma fábrica Supplier<T>. As informações que o apagamento removeu precisam ser fornecidas pelo chamador; o compilador não consegue reconstruí-las.

Métodos bridge — a contabilidade oculta do apagamento

Há uma sutileza onde o apagamento interage com a sobreposição. Suponha que você tenha:

interface Container<T> {
  void put(T value);
}

class IntContainer implements Container<Integer> {
  public void put(Integer value) { ... }
}

No nível da fonte, IntContainer.put(Integer) sobrepõe Container.put(T). Mas após o apagamento, o método da interface tem a assinatura put(Object) — e IntContainer só tem put(Integer). Como o polimorfismo ainda funciona quando alguém chama put por meio de uma referência Container?

O compilador gera um método bridge em IntContainer:

// Generated by the compiler, invisible in source:
public void put(Object value) {
  put((Integer) value);   // delegate to the real one
}

Esse método bridge é o que é chamado quando o despacho polimórfico chega na assinatura apagada. Você não o escreve, não o vê, mas o javap irá mostrá-lo. É a cola que faz o apagamento funcionar com o despacho virtual.

Apagamento e sobrecarga

Uma consequência direta do apagamento: você não pode sobrecarregar dois métodos se suas assinaturas diferem apenas nos parâmetros genéricos, porque após o apagamento eles têm a mesma assinatura:

public void process(List<String> list)  { ... }
public void process(List<Integer> list) { ... }
// ❌ both erase to process(List) — compile error

Este é o mesmo problema latente com sobreposições. Se dois métodos seriam apagados para a mesma assinatura, o compilador se recusa a compilá-los. Não há solução no nível da linguagem — você precisaria de nomes de métodos diferentes ou um parâmetro que seja realmente diferente após o apagamento.

Um exemplo prático: apagamento em ação

O programa demonstra as coisas que decorrem do apagamento — igualdade em tempo de execução de getClass, um instanceof funcionando na forma bruta, e o cast não verificado que o bytecode faz por você em cada leitura.

java— editable, runs on the server

As linhas getClass() confirmam que o tempo de execução não consegue distinguir Box<Integer> de Box<String> — existe apenas uma classe Box. O truque da List em forma bruta é o exemplo clássico de apagamento: o add(99) indevido passa pela verificação de tipo, e a falha aparece na próxima leitura porque é aí que o cast inserido pelo compilador está. A mensagem de exceção até diz qual foi o cast: tentou converter Integer para String.

O que vem a seguir

O apagamento não é um detalhe acadêmico — é a razão por trás de quase todo "você não pode fazer isso" que o compilador lançará em você quando tentar código genérico sofisticado. O capítulo final desta parte cataloga a lista completa dessas restrições e explica cada uma em termos do que o apagamento impede. Continue para Restrições de Generics em Java.

Prática

Prática
Por que o Java rejeita `public T first() { return new T(); }` dentro de uma classe genérica `Foo<T>`?
Por que o Java rejeita `public T first() { return new T(); }` dentro de uma classe genérica `Foo<T>`?
Was this page helpful?