Type Erasure em Java (Remoção de Tipos Genéricos)
Embora o Java permita criar classes e métodos genéricos com parâmetros de tipo, esses tipos não existem de fato em tempo de execução. Na Máquina Virtual Java (JVM), todos os objetos pertencem a classes concretas, e os tipos genéricos são apenas uma abstração de compilação.
Durante o processo de compilação, os parâmetros de tipo são removidos e substituídos por tipos concretos — geralmente Object, ou por tipos definidos como limites (restrições) dos parâmetros.
Esse processo é chamado de type erasure, ou remoção de tipos.
Exemplo básico de remoção de tipo
Considere a seguinte classe genérica:
class Person<T> {
private T id;
private String name;
T getId() { return id; }
String getName() { return name; }
Person(T id, String name) {
this.id = id;
this.name = name;
}
}Como o parâmetro de tipo T não tem restrições, o compilador o substitui por Object.
Após o processo de type erasure, a JVM enxerga algo equivalente a isto:
class Person {
private Object id;
private String name;
Object getId() { return id; }
String getName() { return name; }
Person(Object id, String name) {
this.id = id;
this.name = name;
}
}Assim, pouco importa o tipo genérico usado na criação do objeto:
Person tom = new Person<Integer>(456, "Tom");
Person bob = new Person<String>("qwert", "Bob");Em ambos os casos, em tempo de execução, o tipo T é tratado como Object.
Conversões automáticas após a remoção de tipo
Quando o resultado de um método genérico ou um campo é atribuído a uma variável de tipo específico, o compilador insere automaticamente as conversões necessárias. Por exemplo:
var tom = new Person<Integer>(456, "Tom");
int tomId = tom.getId();
System.out.println(tomId);Após a remoção dos tipos genéricos, o código é tratado internamente como:
var tom = new Person(456, "Tom");
int tomId = (int) tom.getId();
System.out.println(tomId);O compilador gera o cast para garantir a compatibilidade entre o tipo genérico e o tipo real da variável.
Parâmetros de tipo com restrições
Agora, veja um exemplo com limite de tipo (bounded type parameter):
class Message {
private String text;
String getText() { return text; }
Message(String text) {
this.text = text;
}
}
class Messenger<T extends Message> {
private T message;
T getMessage() { return this.message; }
Messenger(T message) { this.message = message; }
void send() {
System.out.println("Enviando mensagem: " + message.getText());
}
}O parâmetro de tipo T tem uma restrição: ele deve ser uma subclasse de Message.
Portanto, após o type erasure, o tipo T é substituído por Message:
class Messenger {
private Message message;
Message getMessage() { return this.message; }
Messenger(Message message) { this.message = message; }
void send() {
System.out.println("Enviando mensagem: " + message.getText());
}
}Múltiplas restrições de tipo
Um parâmetro genérico pode ter vários limites, como uma classe base e um ou mais interfaces. Nesse caso, o compilador substitui o tipo genérico pelo primeiro limite declarado. Se for necessário acessar membros definidos nos outros limites, o compilador insere casts apropriados.
Exemplo:
class Messenger<T extends Message & Printable> {
void sendMessage(T message) {
message.print();
}
}
interface Printable {
void print();
}
class Message {
private String text;
String getText() { return text; }
Message(String text) {
this.text = text;
}
}Aqui, T deve representar um tipo que herda de Message e implementa Printable.
Após o type erasure, o compilador gera algo equivalente a:
class Messenger {
void sendMessage(Message message) {
((Printable) message).print();
}
}O parâmetro T é substituído por Message (o primeiro limite),
e para acessar os métodos de Printable, o compilador insere uma conversão explícita.
Resumo
- O type erasure remove os tipos genéricos durante a compilação, substituindo-os por
Objectou pelo primeiro limite definido. - A JVM não mantém informações de tipo genérico em tempo de execução.
- O compilador adiciona casts automaticamente para preservar a segurança de tipos.
- Em parâmetros genéricos com múltiplas restrições, apenas o primeiro limite é usado como tipo base, e os demais são acessados por conversões.
- Esse comportamento garante compatibilidade retroativa com código Java anterior à introdução de genéricos (Java 5).