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