Що таке type erasure (стирання типів)
У виконуваному байткоді (.class-файлі) та під час роботи JVM дженериків практично не існує. Віртуальна машина оперує сирими типами (raw types) та вставленими компілятором інстру...
🟢 Junior Level
Type Erasure (стирання типів) — це архітектурний механізм компілятора Java, за якого вся інформація про аргументи дженерик-типів видаляється (стирається) на етапі компіляції.
У виконуваному байткоді (.class-файлі) та під час роботи JVM дженериків практично не існує. Віртуальна машина оперує сирими типами (raw types) та вставленими компілятором інструкціями приведення типів (checkcast).
// Що пише розробник:
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
// Що насправді компілюється в байткод:
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // Компілятор неявно вставив приведення типу!
Навіщо було обрано стирання типів
У 2004 році при релізі Java 5 стояло завдання забезпечити 100% зворотну сумісність з мільйонами рядків коду на Java 1.4. Завдяки стиранню типів, нові дженерик-бібліотеки могли прозоро працювати зі старими скомпільованими модулями без перезбирання всієї екосистеми.
🟡 Middle Level
1. Правила стирання типів (JLS §4.6)
Компілятор замінює змінну типу $T$ за суворим алгоритмом:
- Необмежений тип (
<T>) стирається доjava.lang.Object:public class Box<T> { private T val; } // Перетворюється на: public class Box { private Object val; } - Обмежений зверху тип (
<T extends Number>) стирається до своєї першої межі (First Bound):public class NumberBox<T extends Number> { private T val; } // Перетворюється на: public class NumberBox { private Number val; } - Множинні обмеження (
<T extends Comparable<T> & Serializable>) стираються до найпершого типу в списку (в цьому випадку доComparable).
2. Практичні обмеження, викликані стиранням типів
Через те, що в рантаймі інформація про конкретний $T$ відсутня, в Java заборонені наступні конструкції:
public class Restrictions<T> {
// ❌ 1. Не можна створити екземпляр через new:
// T obj = new T(); // Помилка: JVM не знає конструктор T
// ❌ 2. Не можна створити масив дженерик-типу:
// T[] arr = new T[10]; // Помилка: масиви reified, їм потрібен точний тип у рантаймі
// ❌ 3. Не можна перевіряти точний дженерик-тип через instanceof:
// if (x instanceof List<String>) // Помилка: в рантаймі це просто List
// ❌ 4. Не можна використовувати T у статичному контексті:
// private static T instance; // Помилка: static належить класу, а не екземпляру з типом T
// ❌ 5. Не можна перевантажувати методи, якщо їхні типи збігаються після стирання:
// void print(List<String> l) {}
// void print(List<Integer> l) {} // Помилка: обидва стираються в print(List)
}
🔴 Senior Level
Анатомія байткоду: Мостові методи (Bridge Methods)
Головна архітектурна проблема стирання типів — загроза поліморфізму віртуальних викликів.
Розглянемо наслідування:
public class Node<T> {
public void setData(T data) { /* ... */ }
}
public class StringNode extends Node<String> {
@Override
public void setData(String data) { /* ... */ }
}
Після компіляції та стирання типів:
- У
Nodeметод перетворюється наpublic void setData(Object data). - У
StringNodeоголошено методpublic void setData(String data). - Для віртуальної машини
setData(Object)таsetData(String)— це два абсолютно різні перевантажені методи, а не перевизначення (override)! Поліморфізм ламається: при виклику через базове посиланняNode n = new StringNode(); n.setData("test");метод уStringNodeне викликався б.
Рішення компілятора: Синтетичний Bridge Method
Компілятор автоматично генерує в нащадку прихований мостовий метод (ACC_BRIDGE | ACC_SYNTHETIC):
// Синтетичний метод, що генерується javac у StringNode.class:
public void setData(Object data) {
// 1. Перевіряє тип аргументу через checkcast
// 2. Делегує виклик користувацькому методу з правильною сигнатурою:
this.setData((String) data);
}
Де дженерики зберігаються: Атрибут Signature
Хоча типи стираються з інструкцій виконання методів, метадані сигнатур зберігаються у файлі .class в атрибуті Signature:
- Для полів класу (
Field.getGenericType()). - Для параметрів та типів повернення методів (
Method.getGenericReturnType()). - Для предків та інтерфейсів класу (
Class.getGenericSuperclass()).
ClassFile {
...
fields {
field_info {
u2 name_index;
...
attributes {
Signature_attribute { // <--- Зберігає Ljava/util/List<Ljava/lang/String;>;
u2 attribute_name_index;
u2 signature_index;
}
}
}
}
}
Саме тому патерн TypeToken (Jackson TypeReference, Spring ResolvableType) працює: створюючи анонімний підклас new TypeReference<List<User>>() {}, ми змушуємо компілятор зберегти сигнатуру предка в метаданих класу, звідки рефлексія витягує параметри типів у runtime!
4 Tricky Questions
1. Якщо у типу кілька обмежень (<T extends Comparable<T> & Serializable>), до якого типу компілятор зітре T?
Відповідь: Компілятор завжди стирає тип $T$ до першої оголошеної межі (First Bound).
У виразі <T extends Comparable<T> & Serializable> першою межею є Comparable, тому в байткоді поле або метод перетвориться на Comparable. В усіх місцях, де вимагається використання другого інтерфейсу (Serializable), компілятор генеруватиме додаткові явні інструкції приведення типів checkcast.
Саме тому специфікація Java вимагає, щоб клас (якщо він є серед bounds) завжди стояв на першому місці: <T extends Number & Runnable> допустимо, а <T extends Runnable & Number> викличе помилку компіляції.
2. Чи зберігається інформація про дженерики для локальних змінних методів всередині байткоду?
Відповідь: Ні, для локальних змінних метадані дженериків не зберігаються.
Атрибут Signature генерується компілятором лише для елементів постійної структури класу: для оголошень полів, заголовків методів та інтерфейсів. Локальні змінні всередині тіл методів стираються повністю та остаточно: List<String> list = new ArrayList<>() у байткоді стає локальною змінною типу java.util.List. Жодна рефлексія в рантаймі не здатна дізнатися, який тип було вказано в локальній змінній.
3. Чи можна оголосити два перевантажені методи, що відрізняються лише аргументами дженериків (void sort(List<String> l) та void sort(List<Integer> l))?
Відповідь:
Ні, це викличе помилку компіляції: «both methods have the same erasure».
Після стирання типів обидва методи отримають абсолютно ідентичну сигнатуру на рівні JVM: void sort(List l). Наявність двох методів з однаковим ім’ям та однаковим дескриптором параметрів в одному класі заборонена специфікацією байткоду Java (JVMS).
Обхідні шляхи: або дати методам різні імена (sortStrings, sortIntegers), або використовувати фіктивний dummy-параметр.
4. Чому в Java дозволено перевантаження методів, що відрізняються типом повернення на рівні байткоду (коваріантні типи повернення), хоча у вихідному Java-коді це заборонено?
Відповідь: У специфікації мови Java (JLS) сигнатура методу складається лише з його імені та типів параметрів (тип повернення до сигнатури не входить). Тому в Java-коді не можна оголосити два методи з однаковим ім’ям та аргументами, але різним типом повернення.
Однак у специфікації віртуальної машини (JVMS) дескриптор методу включає в себе і тип повернення: (Ljava/lang/Object;)Ljava/lang/String; та (Ljava/lang/Object;)Ljava/lang/Object; для JVM є двома різними методами. Саме на цьому трюку компілятор будує коваріантні типи повернення та мостові методи (Bridge Methods).
🎯 Шпаргалка для інтерв’ю
Головне про Type Erasure за 1 хвилину
- Що це: Процес видалення параметрів дженериків при компіляції в байткод.
- У що стираються типи:
- Без обмежень:
T$\rightarrow$Object. - З обмеженням:
T extends Number$\rightarrow$Number(перший bound).
- Без обмежень:
- Головна мета: 100% бінарна зворотна сумісність коду Java 5 зі старим кодом Java 1.4.
- Наслідки стирання:
- Не можна:
new T(),new T[],x instanceof List<String>,class Ex<T> extends Exception. - Не можна перевантажувати методи з однаковим стиранням:
foo(List<String>)таfoo(List<Integer>).
- Не можна:
- Bridge Methods: Синтетичні методи, що генеруються компілятором для збереження віртуального поліморфізму в байткоді.
- Збереження метаданих: В атрибуті
Signatureclass-файлу (для полів, методів та класів). Використовується патерномTypeTokenу Jackson/Spring.