Вопрос 13 · Раздел 20

Что такое type erasure (стирание типов)

После компиляции JVM не знает, какой тип был указан в дженерике.

Версии по языкам: English Russian Ukrainian

🟢 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 и зачем они нужны]]