Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.2.3. Type Erasure

Стирание типов в runtime

Материалы

ТипСсылка
Документссылка
Видеоссылка

Что такое Type Erasure

Type Erasure (стирание типов) - это процесс преобразования параметризованных типов (generics) в обычные классы и интерфейсы во время компиляции.

Зачем нужен Type Erasure?

  1. Обратная совместимость (Backward Compatibility)

    • Код с дженериками может работать со старым кодом без дженериков
    • Старый байт-код может работать с новой JVM
    • Библиотеки с дженериками совместимы со старыми версиями
  2. Migration Compatibility

    • Постепенный переход от legacy кода к коду с дженериками
    • Не нужно переписывать весь код сразу
  3. Единый байт-код

    • Нет дублирования классов для разных типов (как в 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?

  1. Override метода с параметризованным типом
  2. 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:

  1. ✅ Primitive types: int, double, boolean
  2. ✅ Non-generic классы и интерфейсы: String, Object, Integer
  3. ✅ Raw types: List, Map
  4. ✅ Параметризованные типы с unbounded wildcards: List<?>, Map<?, ?>
  5. ✅ Массивы reifiable типов: int[], String[], List<?>[]

Типы, которые НЕ reifiable:

  1. ❌ Параметризованные типы: List<String>, Map<String, Integer>
  2. ❌ Type variables: T, E, K, V
  3. ❌ Параметризованные типы с type variables: List<T>
  4. ❌ Bounded wildcards: List<? extends Number>, List<? super Integer>

Зачем нужна reifiability?

Используется для:

  • Создания массивов: new String[10] ✅, new List<String>[10]
  • instanceof checks: 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?

  1. Совместимость с legacy кодом (до Java 5)
  2. 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

  1. Unchecked warnings игнорируются
  2. Raw types используются
  3. Unchecked casts выполняются
  4. 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

  1. ✅ Не игнорируйте unchecked warnings
  2. ✅ Не используйте raw types
  3. ✅ Будьте осторожны с unchecked casts
  4. ✅ Используйте @SafeVarargs только когда метод точно безопасен
  5. ✅ Используйте @SuppressWarnings("unchecked") локально и осторожно

Сравнение с другими языками

Java vs C++

Java GenericsC++ Templates
Type ErasureCode Generation (каждый тип = отдельный класс)
Один байт-код для всех типовОтдельный код для каждого типа
Информация о типе теряетсяИнформация о типе сохраняется
Меньший размер программыБольший размер программы
Ограничения в runtimeБольше возможностей в runtime
Обратная совместимостьНет обратной совместимости

Java vs C#

JavaC#
Type ErasureReified 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 (рекомендуется):

  1. Понимайте ограничения Type Erasure

    • Не пытайтесь получить информацию о типе в runtime
    • Используйте Class или TypeToken когда нужна информация о типе
  2. Используйте параметризованные типы везде

    List<String> list = new ArrayList<>();  // ✅
    
  3. Обращайте внимание на unchecked warnings

    • Не игнорируйте без причины
    • Понимайте почему warning возникает
  4. Используйте bounded type parameters

    <T extends Number> T add(T a, T b)  // ✅
    
  5. Передавайте Class когда нужна реификация

    public <T> T create(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
    

❌ DON’T (не рекомендуется):

  1. Не используйте raw types

    List list = new ArrayList();  // ❌ Raw type
    
  2. Не пытайтесь создать массив параметризованных типов

    List<String>[] array = new List<String>[10];  // ❌ Не скомпилируется
    
  3. Не используйте instanceof с параметризованными типами

    if (obj instanceof List<String>) { }  // ❌ Не скомпилируется
    
  4. Не перегружайте методы с одинаковым erasure

    void process(List<String> list) { }   // ❌
    void process(List<Integer> list) { }  // Same erasure
    
  5. Не полагайтесь на тип параметра в static контексте

    public class Box<T> {
        private static T instance;  // ❌ Не скомпилируется
    }
    

Заключение

Ключевые моменты:

  1. Type Erasure - это компромисс между:

    • Обратной совместимостью ✅
    • Информацией о типе в runtime ❌
  2. В runtime:

    • List<String> = List<Integer> = List
    • Вся информация о type arguments стирается
  3. Компилятор вставляет casts автоматически:

    String s = list.get(0);  →  String s = (String)list.get(0);
    
  4. Bridge methods сохраняют полиморфизм после erasure

  5. Raw types существуют для совместимости, но не используйте их в новом коде

Важно помнить:

  • Generics - это compile-time feature
  • Type safety проверяется только во время компиляции
  • В runtime JVM видит только erased types
  • Unchecked warnings - признак потенциальных проблем

Type Erasure - фундаментальная особенность дженериков в Java, которую нужно понимать для эффективного использования системы типов.