Що таке 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 за дженерик типом
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 і навіщо вони потрібні]]