Что такое type erasure (стирание типов)
После компиляции JVM не знает, какой тип был указан в дженерике.
🟢 Junior Level
Type erasure (стирание типов) — это процесс, при котором компилятор Java удаляет информацию о дженерик-типах во время компиляции.
После компиляции JVM не знает, какой тип был указан в дженерике.
// Ваш код:
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// После компиляции (type erasure):
List strings = new ArrayList(); // String -> Object
List integers = new ArrayList(); // Integer -> Object
// JVM видит просто List, без информации о типе!
Зачем это нужно:
- Совместимость со старым кодом (до Java 5)
- Никакого overhead в runtime
- Код работает так же быстро
🟡 Middle Level
Как это работает
Компилятор заменяет type parameters на их bounds (или Object, если bound нет):
// Исходный код
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// После type erasure
public class Box {
private Object value; // T -> Object (нет bound)
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
Bounded type erasure:
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() { return value.doubleValue(); }
}
// После erasure: T -> Number (первый bound)
public class NumberBox {
private Number value;
public double doubleValue() { return value.doubleValue(); }
}
Компилятор добавляет cast:
Box<String> box = new Box<>();
box.set("Hello");
String s = box.get();
// После компиляции:
Box box = new Box();
box.set("Hello");
String s = (String) box.get(); // неявный cast добавлен
Ограничения из-за type erasure
1. Нельзя создать экземпляр T:
public class Box<T> {
// ❌ Нельзя
public Box() {
value = new T(); // compilation error
}
}
2. Нельзя instanceof T:
public void check(Object obj) {
// ❌ Нельзя
if (obj instanceof T) { } // compilation error
}
3. Нельзя массив T:
public class Box<T> {
// ❌ Нельзя
private T[] array = new T[10]; // compilation error
// ✅ Решение
private T[] array = (T[]) new Object[10];
}
4. Нельзя static поле с T:
public class Box<T> {
// ❌ Нельзя — static контекст не знает о T
private static T value; // compilation error
}
🔴 Senior Level
Internal Implementation
JLS Specification (4.6):
Type erasure mapping:
1. Type parameter -> первый bound (или Object)
2. Parameterized type -> erasure of raw type
3. Nested type -> recursively applied
Bridge methods:
public class Node<T extends Comparable<T>> {
private T data;
public void setData(T data) { this.data = data; }
public T getData() { return data; }
}
public class DateNode extends Node<Date> {
@Override
public void setData(Date data) { super.setData(data); }
@Override
public Date getData() { return super.getData(); }
}
// После erasure:
class Node {
private Comparable data;
public void setData(Comparable data) { }
public Comparable getData() { return data; }
}
class DateNode extends Node {
public void setData(Date data) { super.setData(data); }
public Date getData() { return super.getData(); }
// Bridge method для сохранения полиморфизма:
public void setData(Comparable data) { setData((Date) data); }
public Comparable getData() { return getData(); }
// В DateNode компилятор генерирует:
// public Object getData() { return this.getData(); } // вызывает Date getData()
// Это НЕ рекурсия — вызывается перегруженный метод с другой сигнатурой возврата.
}
Reflection:
// Проверка через reflection
public void inspect() {
List<String> list = new ArrayList<>();
// Type erasure — runtime не знает о String
Type genericType = list.getClass().getGenericSuperclass();
System.out.println(genericType); // java.util.AbstractList<E>
// Но можно получить информацию из поля/метода
Field field = MyClass.class.getDeclaredField("stringList");
ParameterizedType pt = (ParameterizedType) field.getGenericType();
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
}
Архитектурные Trade-offs
Type erasure vs reified generics:
| Type Erasure (Java) | Reified (C#, Kotlin) |
|---|---|
| Бинарная совместимость | Полная информация в runtime |
| Zero overhead | Runtime overhead |
| Ограничения (new T, instanceof) | Полная функциональность |
| Bridge methods | Нет bridge methods |
Edge Cases
1. Generic array creation exception:
public class GenericArray<T> {
private T[] elements;
public GenericArray(int size) {
// Нельзя new T[], но можно через Class
elements = (T[]) Array.newInstance(
Object.class, size
);
// Это всё ещё unchecked cast. Array.newInstance(Object.class) создаёт Object[],
// не T[]. Типобезопасный подход требует Class<T>.
}
}
2. Generic exception handling:
public class ExceptionHandler {
// Type erasure — нельзя catch по generic типу
public void handle(Exception e) {
// ❌ Нельзя
// if (e instanceof RuntimeException<String>) { }
// ✅ Проверка через class
if (e instanceof RuntimeException) { }
}
}
3. Heap pollution:
// Heap pollution — когда generic коллекция содержит неправильный тип
List rawList = new ArrayList();
rawList.add("Hello");
List<String> stringList = rawList; // unchecked warning
String s = stringList.get(0); // OK
rawList.add(42); // heap pollution — в списке строк появился Integer
Integer i = (Integer) stringList.get(1); // ClassCastException!
// @SafeVarargs помогает
@SafeVarargs
public static <T> void safeMethod(List<T>... lists) { }
Производительность
Type erasure overhead:
- Compile time: проверка типов
- Runtime: Zero overhead
- Memory: Нет дополнительных данных
- Cast: Неявные cast при доступе
Бенчмарк:
Операция | С дженериками | Без дженериков
------------------|---------------|-----------------
List.get() | 1 ns | 1 ns (+ cast)
Map.put() | 5 ns | 5 ns
Type check | compile time | runtime (ClassCastException risk)
Production Experience
Workarounds для type erasure:
// 1. Передача Class<T> для создания экземпляров
public class Factory<T> {
private final Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
// 2. Type token для generics
public class TypeReference<T> {
private final Type type;
protected TypeReference() {
ParameterizedType pt = (ParameterizedType)
getClass().getGenericSuperclass();
this.type = pt.getActualTypeArguments()[0];
}
public Type getType() { return type; }
}
// Использование (Jackson style)
User user = mapper.readValue(json, new TypeReference<User>() {});
Best Practices
// ✅ Используйте Class<T> для создания экземпляров
public <T> T create(Class<T> type) { return type.newInstance(); }
// ✅ Wildcards для flexibility
public void process(List<? extends Number> numbers) { }
// ✅ @SafeVarargs для generic varargs
@SafeVarargs
public static <T> void safeMethod(T... items) { }
// ❌ new T()
// ❌ instanceof T
// ❌ new T[]
// ❌ Static контекст с T
🎯 Шпаргалка для интервью
Обязательно знать:
- Type erasure — компилятор заменяет T на Object (или первый bound) при компиляции
- Зачем: бинарная совместимость с pre-Java 5 кодом, zero runtime overhead
- Ограничения: нельзя
new T(),instanceof T,new T[], static поле с T - Bridge methods — компилятор создаёт их для сохранения полиморфизма после erasure
- Bounded erasure:
<T extends Number>— T заменяется на Number, не Object - Heap pollution — когда generic коллекция содержит элементы неправильного типа
Частые уточняющие вопросы:
- Почему нельзя new T()? — После erasure T -> Object, компилятор не знает какой конструктор вызывать
- Можно ли узнать тип T в runtime? — Нет напрямую, но можно через Class
или TypeReference - Что такое bridge method? — Синтетический метод с erased signature, делегирующий оригинальному
- Type erasure vs reified generics (C#)? — Java: compile-time only, C#: полная информация в runtime
Красные флаги (НЕ говорить):
- ❌ “JVM проверяет типы дженериков в runtime” — Type erasure, JVM не видит generic types
- ❌ “new T() работает через reflection” — Запрещено на уровне компилятора
- ❌ “Type erasure добавляет overhead” — Zero overhead, никаких дополнительных данных
- ❌ “Можно instanceof T проверить” — Compilation error, тип стёрт
Связанные темы:
- [[11. Что такое дженерики (Generics) в Java]]
- [[12. В чём преимущества использования дженериков]]
- [[14. Можно ли создать массив дженерик-типа]]
- [[20. Что произойдёт при попытке создать экземпляр дженерик-типа через new T()]]
- [[25. Что такое bridge methods и зачем они нужны]]