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:
- 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.
- 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:
- Apaga cada parâmetro de tipo para seu limite mais à esquerda — ou para
Objectse não houver limite. - 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(); // sourcetorna-se (após o apagamento):
Box b = new Box(42);
int x = (Integer) b.get(); // compiler inserted the castO 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.classNã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 worksVocê 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.
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 errorEste é 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.
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.