📋 Розділ 20 · Питання #19

Що таке raw types і чому їх слід уникати

До появи дженериків у Java 5 усі колекції були «сирими». Сирі типи залишені в Java виключно для сумісності зі старим legacy-кодом.


🟢 Junior Level

Raw Type (сирий тип) — це використання параметризованого класу або інтерфейсу без зазначення аргументів типів.

До появи дженериків у Java 5 усі колекції були «сирими». Сирі типи залишені в Java виключно для сумісності зі старим legacy-кодом.

// ✅ Параметризований тип (безпечно):
List<String> safeList = new ArrayList<>();
safeList.add("Hello");
String s1 = safeList.get(0); // Тип гарантовано компілятором

// ❌ Raw type (небезпечно!):
List rawList = new ArrayList(); // Попередження компілятора: raw type
rawList.add("Hello");
rawList.add(42); // Компілятор пропускає число в колекцію рядків!

String s2 = (String) rawList.get(1); // 💥 ClassCastException під час виконання!

3 причини уникати Raw Types

  1. Повна втрата типобезпеки: компілятор вимикає перевірки типів при вставці елементів.
  2. Ризик ClassCastException у production: помилка проявляється не в момент додавання зіпсованих даних, а пізніше — при спробі їх прочитати.
  3. Необхідність ручного приведення типів (cast): код захаращується небезпечними приведеннями.

🟡 Middle Level

1. Каскадне стирання членів класу (Erasure of Member Types)

Маловідомий, але критично небезпечний ефект сирих типів (JLS §4.8): при використанні raw type стираються типи повернення ВСІХ методів класу, навіть якщо вони явно не залежать від змінної типу $T$!

public class Box<T> {
    private T value;

    // Метод ВЗАГАЛІ не використовує тип T:
    public List<String> getTags() {
        return List.of("tag1", "tag2");
    }
}

// 1. Використання з параметром типу:
Box<Integer> typedBox = new Box<>();
List<String> tags = typedBox.getTags(); // ✅ Тип збережено: List<String>

// 2. Використання через Raw Type:
Box rawBox = new Box();
// ❌ Помилка компіляції або Unchecked warning:
// Метод getTags() повернув СИРИЙ List, а не List<String>!
List<String> rawTags = rawBox.getTags(); 

Використання сирого типу «отруює» весь клас, перетворюючи навіть суворо типізовані методи на їхні до-дженерикові аналоги.

2. Принципова різниця: List (Raw) vs List<?> (Wildcard)

Багато розробників помилково вважають, що сирий тип List та підстановочний тип List<?> є еквівалентними. Це принципово не так:

// ❌ Raw Type: контроль типів вимкнено
List raw = new ArrayList<String>();
raw.add(12345); // ✅ Компільовано (з warning). Безпеку зруйновано!

// ✅ Unbounded Wildcard: контроль типів УВІМКНЕНО
List<?> wildcard = new ArrayList<String>();
// wildcard.add(12345);   // ❌ Помилка компіляції! Запис заборонено!
// wildcard.add("hello"); // ❌ Помилка компіляції! Тип невідомий!
wildcard.add(null);       // Дозволено лише null
  • List (raw): «Мені байдуже на типи, клади що завгодно» (небезпечно).
  • List<?>: «Тут лежить список деякого конкретного типу, але ми його не знаємо, тому не дозволяємо нічого записувати задля уникнення збоїв» (100% типобезпечно).

🔴 Senior Level

Єдині законні сценарії використання Raw Types

Специфікація мови Java (JLS §4.8) допускає використання сирих типів лише у двох синтаксичних конструкціях:

1. Класові літерали (Class Literals)

Синтаксис літералів класу не дозволяє вказувати параметри типів:

Class<?> clazz1 = List.class; // ✅ Єдино вірний синтаксис

// ❌ Помилки компіляції:
// Class<?> clazz2 = List<String>.class; 
// Class<?> clazz3 = List<?>.class;

2. Оператор instanceof (до появи Pattern Matching)

Через стирання типів JVM у рантаймі не здатна перевірити параметр типу:

if (obj instanceof List) { // ✅ Легально: перевіряється тільки тип List
    List<?> list = (List<?>) obj;
}

// ❌ Помилка компіляції: Cannot perform instanceof check against parameterized type
// if (obj instanceof List<String>) { ... }

(Примітка: Починаючи з Java 16+, в операторі instanceof рекомендується використовувати необмежений wildcard: if (obj instanceof List<?> list)).

Походження Heap Pollution через Raw Types

Сирі типи є головним джерелом Heap Pollution (забруднення купи):

public class PollutionDemo {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        corrupt(strings); // Передаємо безпечний список у метод із сирим типом

        // Код падає тут, хоча в цьому класі не було жодної некоректної дії!
        String s = strings.get(0); // 💥 ClassCastException: Integer cannot be cast to String!
    }

    private static void corrupt(List rawList) {
        rawList.add(42); // Забруднення пам'яті купи
    }
}

4 Tricky Questions

1. Що відбувається з усіма іншими методами класу, якщо звернутися до об’єкта через Raw Type?

Відповідь: Відповідно до JLS §4.8 відбувається каскадне стирання типів (Erasure of Member Types): сигнатури абсолютно всіх методів і полів класу стираються до їхніх базових типів.

Якщо всередині узагальненого класу class Box<T> оголошено метод public List<String> getTags(), який ніяк не використовує змінну типу $T$, то при виклику rawBox.getTags() компілятор поверне сирий List, а не List<String>. Використання сирого типу знімає дженерик-типізацію з усього інтерфейсу класу цілком.

2. У чому фундаментальна різниця між сирим типом List та підстановочним типом List<?>?

Відповідь:

  • List (сирий тип) повністю вимикає систему перевірки типів: у нього дозволено додати об’єкт будь-якого типу (rawList.add(123)), породжуючи ризик забруднення купи (Heap Pollution) та відкладених ClassCastException.
  • List<?> (unbounded wildcard) суворо дотримується типобезпеки: він повідомляє компілятору, що колекція типізована певним, але невідомим типом. Компілятор блокує додавання будь-яких елементів (крім null), гарантуючи, що зіпсувати дані неможливо.

3. У яких випадках специфікація мови Java вимагає використовувати Raw Types?

Відповідь: Існують два місця, де використання параметрів типів синтаксично заборонено мовою:

  1. У класових літералах (Class Literals): специфікація вимагає писати List.class. Вирази List<String>.class або List<?>.class не компілюються.
  2. В операторі перевірки типів instanceof: через стирання типів у рантаймі доступний лише тип класу, тому перевірка пишеться як obj instanceof List (або obj instanceof List<?>).

4. Чому сирі типи (Raw Types) досі не видалили з мови Java?

Відповідь: Через непохитний принцип зворотної сумісності (Backward Compatibility) екосистеми Java.

У світі досі експлуатуються легасі-бібліотеки, скомпільовані в часи Java 1.4, а також масштабні кодові бази, що взаємодіють зі старими модулями. Повне видалення сирих типів зробило б неможливим використання старих .jar файлів без їхнього переписування. Проте компілятор за замовчуванням позначає використання сирих типів попередженнями [rawtypes] та [unchecked].


🎯 Шпаргалка для інтерв’ю

Головне про Raw Types за 30 секунд

  • Що це: Використання параметризованого класу без типів: List list = new ArrayList().
  • Навіщо існують: Виключно заради зворотної сумісності з кодом Java 1.4.
  • Головні небезпеки:
    • Вимикає перевірку типів при записі $\rightarrow$ ризик ClassCastException у рантаймі.
    • Породжує Heap Pollution.
    • Каскадно стирає типи повернення всіх методів класу (JLS §4.8).
  • Різниця з List<?>: У List можна покласти будь-який об’єкт (небезпечно); у List<?> не можна покласти нічого, крім null (безпечно).
  • Де дозволені: Тільки в класових літералах (List.class) та instanceof List.

Антипатерни

  • ❌ Використання сирих типів у новому коді: замінюйте на List<?> або точні типи.
  • ❌ Придушення @SuppressWarnings("rawtypes") без крайньої необхідності: маскує потенційні падіння в продакшені.

Пов’язані теми