2.2.3. Type Erasure
Стирание типов в runtime
Материалы
Что такое Type Erasure
Type Erasure (стирание типов) - это процесс преобразования параметризованных типов (generics) в обычные классы и интерфейсы во время компиляции.
Зачем нужен Type Erasure?
-
Обратная совместимость (Backward Compatibility)
- Код с дженериками может работать со старым кодом без дженериков
- Старый байт-код может работать с новой JVM
- Библиотеки с дженериками совместимы со старыми версиями
-
Migration Compatibility
- Постепенный переход от legacy кода к коду с дженериками
- Не нужно переписывать весь код сразу
-
Единый байт-код
- Нет дублирования классов для разных типов (как в C++ templates)
- Меньший размер JAR файлов
Компромисс
❌ Цена Type Erasure:
- Информация о типе-параметре недоступна в runtime
- Невозможно создать массив дженериков:
new T[] - Невозможно использовать
instanceofс параметризованными типами - Проблемы с overloading методов
Правила Type Erasure
Type Erasure обозначается как |T| (erasure типа T).
1. Erasure параметризованного типа
|G<T1, T2, ..., Tn>| = |G|
Правило: Параметризованный тип превращается в raw type (erasure самого класса).
// До erasure (compile-time)
List<String> list = new ArrayList<String>();
Map<String, Integer> map = new HashMap<String, Integer>();
// После erasure (runtime)
List list = new ArrayList(); // String стерт
Map map = new HashMap(); // String и Integer стерты
2. Erasure вложенного типа
|T.C| = |T|.C
Правило: Сначала стирается внешний тип, внутренний остается.
// До erasure
Outer<String>.Inner inner;
// После erasure
Outer.Inner inner; // String стерт, но Inner остался
3. Erasure массива
|T[]| = |T|[]
Правило: Стирается тип элементов массива.
// До erasure
List<String>[] arrayOfLists;
// После erasure
List[] arrayOfLists; // String стерт, массив List остался
4. Erasure type variable
|T| = erasure первой границы (leftmost bound)
Правило: Type variable заменяется на erasure его первой границы (bound).
// 1. Без ограничений - становится Object
<T> → Object
// 2. С одним ограничением - становится этим типом
<T extends Number> → Number
// 3. С несколькими ограничениями - становится первым
<T extends Number & Serializable & Comparable> → Number
// 4. Type variable с interface bound
<T extends Comparable> → Comparable
Примеры:
class Box<T> {
private T value; // После erasure: Object value
public void set(T v) { // После erasure: void set(Object v)
value = v;
}
public T get() { // После erasure: Object get()
return value;
}
}
// После type erasure класс превращается в:
class Box {
private Object value;
public void set(Object v) {
value = v;
}
public Object get() {
return value;
}
}
class BoundedBox<T extends Number> {
private T value; // После erasure: Number value
public void set(T v) { // После erasure: void set(Number v)
value = v;
}
public T get() { // После erasure: Number get()
return value;
}
}
// После type erasure:
class BoundedBox {
private Number value;
public void set(Number v) {
value = v;
}
public Number get() {
return value;
}
}
5. Erasure других типов
|Primitive| = Primitive
|Class| = Class
|Interface| = Interface
Правило: Не-дженерик типы остаются без изменений.
int x; // int (без изменений)
String s; // String (без изменений)
Object o; // Object (без изменений)
Erasure сигнатур методов
Erasure применяется также к сигнатурам методов:
signature = (имя_метода, erasure_параметров)
Erasure сигнатуры:
- Имя метода остается
- Type parameters стираются
- Return type стирается (если параметризован)
- Параметры метода стираются
// До erasure
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// После erasure
public Comparable max(Comparable a, Comparable b) {
return a.compareTo(b) > 0 ? a : b;
}
// До erasure
public <T> List<T> createList(T... elements) {
return Arrays.asList(elements);
}
// После erasure
public List createList(Object... elements) {
return Arrays.asList(elements);
}
Bridge Methods (мостовые методы)
Компилятор автоматически создает bridge methods для сохранения полиморфизма после erasure.
Проблема без bridge methods
class Node<T> {
public T data;
public void setData(T data) {
this.data = data;
}
}
class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) { // setData(Integer) - новый метод
System.out.println("Setting: " + data);
super.setData(data);
}
}
После erasure:
// Node после erasure
class Node {
public Object data;
public void setData(Object data) { ... }
}
// MyNode после erasure
class MyNode extends Node {
public void setData(Integer data) { ... } // Это НЕ override!
}
Проблема: setData(Integer) не переопределяет setData(Object)!
Решение: Bridge Method
Компилятор создает синтетический bridge method:
class MyNode extends Node {
// Реальный метод
public void setData(Integer data) {
System.out.println("Setting: " + data);
super.setData(data);
}
// Bridge method (создается компилятором)
public void setData(Object data) { // Override родительского метода
setData((Integer) data); // Делегирует к реальному методу
}
}
Bridge method:
- Создается компилятором автоматически
- Имеет сигнатуру родительского метода после erasure
- Делегирует вызов к реальному методу с cast
- Помечен флагом
ACC_BRIDGEиACC_SYNTHETICв байт-коде
Когда создаются bridge methods?
- Override метода с параметризованным типом
- Covariant return types в дженериках
class Node<T> {
public T getData() { return null; }
}
class IntegerNode extends Node<Integer> {
@Override
public Integer getData() { return 42; } // Covariant return
}
// Компилятор создаст bridge:
// public Object getData() {
// return getData(); // Вызовет Integer getData()
// }
Последствия Type Erasure
1. Невозможно получить тип параметра в runtime
public class Box<T> {
public void printType() {
// ❌ Не скомпилируется
System.out.println(T.class);
// ❌ Не скомпилируется
T instance = new T();
// ❌ Runtime будет видеть только Object
if (value instanceof T) { } // Ошибка компиляции
}
}
Почему? В runtime T стирается до Object (или до bound).
2. Невозможно создать массив параметризованного типа
public class GenericArray<T> {
private T[] array;
// ❌ Не скомпилируется
public GenericArray(int size) {
array = new T[size]; // Generic array creation
}
// ✅ Workaround через Object[]
@SuppressWarnings("unchecked")
public GenericArray(int size) {
array = (T[]) new Object[size]; // Unchecked cast warning
}
}
Почему? JVM не знает какой тип создавать в runtime, так как T стерт.
Правильное решение:
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(Class<T> type, int size) {
// Используем reflection и передаем Class<T>
array = (T[]) Array.newInstance(type, size);
}
}
// Использование
GenericArray<String> arr = new GenericArray<>(String.class, 10);
3. Невозможно использовать instanceof с параметризованными типами
public boolean check(Object obj) {
// ❌ Не скомпилируется
if (obj instanceof List<String>) { }
// ✅ Можно проверить только raw type
if (obj instanceof List) { }
// ✅ Можно проверить с unbounded wildcard
if (obj instanceof List<?>) { } // То же что List
}
Почему? В runtime List<String> и List<Integer> неразличимы - оба стерты до List.
4. Проблемы с overloading
public class MyClass {
// ❌ Ошибка компиляции: same erasure
public void process(List<String> list) { }
public void process(List<Integer> list) { }
// После erasure оба метода имеют одинаковую сигнатуру:
// public void process(List list)
}
Почему? После erasure оба метода имеют одинаковую сигнатуру.
Решение: использовать разные имена методов или дополнительные параметры.
public class MyClass {
// ✅ Разные имена
public void processStrings(List<String> list) { }
public void processIntegers(List<Integer> list) { }
// ✅ Дополнительный параметр
public void process(List<String> list, String dummy) { }
public void process(List<Integer> list, Integer dummy) { }
}
5. Невозможно создать generic exception
// ❌ Не скомпилируется
public class GenericException<T> extends Exception {
private T data;
}
// ❌ Нельзя catch параметризованный тип
catch (GenericException<String> e) { }
Почему? JVM проверяет exception types в runtime, а после erasure все параметры стерты.
6. Static контекст не знает о type parameters
public class Box<T> {
// ❌ Не скомпилируется
private static T defaultValue;
// ❌ Не скомпилируется
public static T getDefault() {
return defaultValue;
}
// ❌ Не скомпилируется
public static void process(T value) { }
}
Почему? Static члены принадлежат классу, а не instance. Type parameter T относится к instance.
Reifiable Types (восстановимые типы)
Reifiable type - тип, чья полная информация доступна в runtime.
Типы, которые reifiable:
- ✅ Primitive types:
int,double,boolean - ✅ Non-generic классы и интерфейсы:
String,Object,Integer - ✅ Raw types:
List,Map - ✅ Параметризованные типы с unbounded wildcards:
List<?>,Map<?, ?> - ✅ Массивы reifiable типов:
int[],String[],List<?>[]
Типы, которые НЕ reifiable:
- ❌ Параметризованные типы:
List<String>,Map<String, Integer> - ❌ Type variables:
T,E,K,V - ❌ Параметризованные типы с type variables:
List<T> - ❌ Bounded wildcards:
List<? extends Number>,List<? super Integer>
Зачем нужна reifiability?
Используется для:
- Создания массивов:
new String[10]✅,new List<String>[10]❌ instanceofchecks:obj instanceof String✅,obj instanceof List<String>❌- Проверки типов в runtime
// ✅ Reifiable - можно создать массив
String[] strings = new String[10];
List[] lists = new List[10]; // Raw type - reifiable
// ❌ Non-reifiable - нельзя создать массив
List<String>[] arrayOfLists = new List<String>[10]; // Ошибка компиляции
Raw Types (сырые типы)
Raw type - это использование дженерик-класса без указания type arguments.
// Generic type
List<String> list = new ArrayList<String>();
// Raw type (без type arguments)
List list = new ArrayList(); // Предупреждение компилятора
Определение Raw Type
Raw type = erasure параметризованного типа.
List<String> → List (raw type)
Map<K, V> → Map (raw type)
Box<T> → Box (raw type)
Зачем нужны Raw Types?
- Совместимость с legacy кодом (до Java 5)
- Migration compatibility
// Старый код (до Java 5)
List list = new ArrayList();
list.add("String");
list.add(Integer.valueOf(42)); // Можно добавить что угодно
// Новый код может работать со старым
List<String> typedList = list; // Unchecked warning
Проблемы с Raw Types
List rawList = new ArrayList();
List<String> stringList = rawList; // Unchecked warning
rawList.add(42); // Ошибка не видна компилятору
String s = stringList.get(0); // ClassCastException в runtime!
Unchecked Warnings
Компилятор выдает unchecked warnings при:
- Присваивании raw type к параметризованному типу
- Вызове методов на raw type, где erasure меняет сигнатуру
- Приведении типов (cast) к параметризованному типу
List rawList = new ArrayList();
List<String> stringList = rawList; // Unchecked assignment warning
rawList.add("test"); // Unchecked call warning
Не используйте raw types в новом коде! Это deprecated practice.
Workarounds для ограничений Type Erasure
1. Передача Class для создания экземпляров
public class Factory<T> {
private Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
// Использование
Factory<String> factory = new Factory<>(String.class);
String str = factory.create();
2. Type Token Pattern
public class TypeReference<T> {
private final Type type;
protected TypeReference() {
Type superclass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// Использование
TypeReference<List<String>> typeRef = new TypeReference<List<String>>() {};
Type type = typeRef.getType(); // List<String> сохранен!
Это работает потому что анонимный класс сохраняет информацию о параметризованном superclass.
3. Супер Type Token (библиотеки Guava, Jackson)
// Guava
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
Type type = token.getType();
// Jackson
JavaType type = mapper.getTypeFactory()
.constructCollectionType(List.class, String.class);
4. Реификация через массивы
@SuppressWarnings("unchecked")
public <T> T[] createArray(T... elements) {
return elements; // Компилятор создаст массив нужного типа
}
// Использование
String[] strings = createArray("a", "b", "c"); // T = String
Heap Pollution (загрязнение кучи)
Heap pollution - ситуация когда переменная параметризованного типа ссылается на объект не того типа.
public static void addToList(List list, Object obj) {
list.add(obj); // Unchecked warning
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
addToList(strings, 42); // Добавили Integer в List<String>
// Heap pollution! strings содержит Integer
String s = strings.get(0); // ClassCastException в runtime
}
Причины Heap Pollution
- Unchecked warnings игнорируются
- Raw types используются
- Unchecked casts выполняются
- Varargs с дженериками
// Varargs создает массив
@SafeVarargs // Подавляет предупреждение
public static <T> void addAll(List<T> list, T... elements) {
for (T element : elements) {
list.add(element);
}
}
// Проблема: компилятор создаст Object[]
List<String> strings = new ArrayList<>();
addAll(strings, "a", "b"); // OK
// Но можно сделать так:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
addAll(strings, "a", "b");
addAll(numbers, 1, 2);
Как избежать Heap Pollution
- ✅ Не игнорируйте unchecked warnings
- ✅ Не используйте raw types
- ✅ Будьте осторожны с unchecked casts
- ✅ Используйте
@SafeVarargsтолько когда метод точно безопасен - ✅ Используйте
@SuppressWarnings("unchecked")локально и осторожно
Сравнение с другими языками
Java vs C++
| Java Generics | C++ Templates |
|---|---|
| Type Erasure | Code Generation (каждый тип = отдельный класс) |
| Один байт-код для всех типов | Отдельный код для каждого типа |
| Информация о типе теряется | Информация о типе сохраняется |
| Меньший размер программы | Больший размер программы |
| Ограничения в runtime | Больше возможностей в runtime |
| Обратная совместимость | Нет обратной совместимости |
Java vs C#
| Java | C# |
|---|---|
| Type Erasure | Reified Generics |
| Информация стирается | Информация сохраняется в runtime |
Нельзя: new T[] | Можно: new T[] |
Нельзя: T.class | Можно: typeof(T) |
| Только reference types (до Java 10+) | Value types и reference types |
| Bridge methods | Нет bridge methods |
Примеры Type Erasure в действии
Пример 1: Generic класс
// Исходный код
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// После erasure
public class Pair {
private Object key;
private Object value;
public Pair(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() { return key; }
public Object getValue() { return value; }
}
Пример 2: Bounded type parameter
// Исходный код
public class NumberBox<T extends Number> {
private T value;
public void set(T value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue(); // Можем вызвать методы Number
}
}
// После erasure
public class NumberBox {
private Number value; // T стерся до Number
public void set(Number value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue();
}
}
Пример 3: Generic метод
// Исходный код
public <T> T first(List<T> list) {
return list.get(0);
}
// После erasure
public Object first(List list) {
return list.get(0);
}
// Использование
List<String> strings = Arrays.asList("a", "b");
String s = first(strings); // Компилятор вставит cast: (String)first(strings)
Пример 4: Множественные bounds
// Исходный код
public <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// После erasure (первая граница = Number)
public Number max(Number a, Number b) {
return ((Comparable)a).compareTo(b) > 0 ? a : b;
}
Best Practices
✅ DO (рекомендуется):
-
Понимайте ограничения Type Erasure
- Не пытайтесь получить информацию о типе в runtime
- Используйте Class
или TypeToken когда нужна информация о типе
-
Используйте параметризованные типы везде
List<String> list = new ArrayList<>(); // ✅ -
Обращайте внимание на unchecked warnings
- Не игнорируйте без причины
- Понимайте почему warning возникает
-
Используйте bounded type parameters
<T extends Number> T add(T a, T b) // ✅ -
Передавайте Class
когда нужна реификация public <T> T create(Class<T> clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); }
❌ DON’T (не рекомендуется):
-
Не используйте raw types
List list = new ArrayList(); // ❌ Raw type -
Не пытайтесь создать массив параметризованных типов
List<String>[] array = new List<String>[10]; // ❌ Не скомпилируется -
Не используйте instanceof с параметризованными типами
if (obj instanceof List<String>) { } // ❌ Не скомпилируется -
Не перегружайте методы с одинаковым erasure
void process(List<String> list) { } // ❌ void process(List<Integer> list) { } // Same erasure -
Не полагайтесь на тип параметра в static контексте
public class Box<T> { private static T instance; // ❌ Не скомпилируется }
Заключение
Ключевые моменты:
-
Type Erasure - это компромисс между:
- Обратной совместимостью ✅
- Информацией о типе в runtime ❌
-
В runtime:
List<String>=List<Integer>=List- Вся информация о type arguments стирается
-
Компилятор вставляет casts автоматически:
String s = list.get(0); → String s = (String)list.get(0); -
Bridge methods сохраняют полиморфизм после erasure
-
Raw types существуют для совместимости, но не используйте их в новом коде
Важно помнить:
- Generics - это compile-time feature
- Type safety проверяется только во время компиляции
- В runtime JVM видит только erased types
- Unchecked warnings - признак потенциальных проблем
Type Erasure - фундаментальная особенность дженериков в Java, которую нужно понимать для эффективного использования системы типов.