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

Чи можна створити масив дженерик-типу

Спроба написати new T[10] або new List[10] викличе помилку компіляції: «generic array creation».


🟢 Junior Level

Ні, безпосередньо створити масив дженерик-типу не можна.

Спроба написати new T[10] або new List<String>[10] викличе помилку компіляції: «generic array creation».

public class Box<T> {
    // ❌ Помилка компіляції: generic array creation
    private T[] array = new T[10]; 

    // ❌ Помилка компіляції: generic array creation
    private List<String>[] listArray = new List<String>[10]; 
}

У чому фундаментальна причина?

Масиви та дженерики в Java побудовані на взаємовиключних принципах:

  1. Масиви опредметнені (Reified): вони знають свій точний тип елементів під час виконання програми (в рантаймі) і перевіряють кожен записуваний елемент (ArrayStoreException).
  2. Дженерики стираються (Type Erasure): інформація про типи видаляється під час компіляції. У рантаймі тип T перетворюється просто на Object.

Оскільки JVM у рантаймі не знає, який саме тип ховається за $T$, вона фізично не може виділити в пам’яті масив потрібного типу з перевіркою меж.


🟡 Middle Level

Небезпека коваріантності масивів та втрата типобезпеки

Уявімо, що Java дозволила б створювати масиви дженериків. Оскільки масиви в Java коваріантні (String[] можна присвоїти Object[]), виникла б дірка в безпеці типів без жодного попередження компілятора:

// Гіпотетичний код, якби new List<String>[5] був дозволений:
List<String>[] stringLists = new List<String>[5]; // 1. Створюємо масив списків рядків

Object[] objects = stringLists; // 2. Легально: масиви коваріантні!

// 3. JVM перевіряє запис: в Object[] кладеться List. 
// Але через стирання типів у рантаймі List<Integer> — це просто List!
// ArrayStoreException НЕ викидається!
objects[0] = List.of(42); 

// 4. Катастрофа: код намагається прочитати рядок з безпечного List<String>,
// але отримує Integer! 
String s = stringLists[0].get(0); // 💥 ClassCastException у чистому дженерик-коді!

Щоб запобігти руйнуванню контракту безпеки типів (Type Safety), автори мови заборонили створення масивів параметризованих типів.

Робочі рішення та патерни обходу

Спосіб 1: Колекції замість масивів (Рекомендація Джошуа Блоха)

У 95% випадків масиви дженериків не потрібні — використовуйте список списків:

List<List<String>> listOfLists = new ArrayList<>();

Спосіб 2: Внутрішній Object[] з кастом при читанні (Патерн ArrayList)

Створюється масив Object[], а приведення типів виконується локально при витягуванні:

public class CustomList<E> {
    private Object[] elements;
    private int size = 0;

    public CustomList(int capacity) {
        elements = new Object[capacity];
    }

    @SuppressWarnings("unchecked")
    public E get(int index) {
        return (E) elements[index]; // Безпечно, якщо запис суворо контролюється
    }
}

Спосіб 3: Створення через Reflection та Class<T>

Якщо масив дійсно необхідно повернути назовні викликаючому коду в типізованому вигляді:

public class ArrayFactory<T> {
    private final T[] array;

    @SuppressWarnings("unchecked")
    public ArrayFactory(Class<T> clazz, int size) {
        // Створює масив реального типу в купі (наприклад, new String[size]):
        this.array = (T[]) java.lang.reflect.Array.newInstance(clazz, size);
    }

    public T[] getArray() {
        return array; // Безпечно віддавати назовні!
    }
}

🔴 Senior Level

Пастка витоку масиву: (T[]) new Object[] vs Array.newInstance

Існує популярний антипатерн створення дженерик-масиву:

public class BadStack<T> {
    private T[] elements;

    @SuppressWarnings("unchecked")
    public BadStack(int size) {
        // Увага: створюється Object[], який насильно кастується до T[]!
        this.elements = (T[]) new Object[size]; 
    }

    public T[] getElements() {
        return elements; // 💣 МІНА УПОВІЛЬНЕНОЇ ДІЇ!
    }
}

Що станеться при використанні:

BadStack<String> stack = new BadStack<>(10);
// Виклик getElements() повертає фактичний Object[]:
String[] arr = stack.getElements(); // 💥 ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;!
  • Масив у пам’яті залишається Object[].
  • Всередині класу BadStack звернення elements[i] працюють, оскільки компілятор транслює їх у (String) elements[i].
  • Але спроба присвоїти посилання на масив змінній типу String[] моментально завершується збоєм, оскільки Object[] не є підтипом String[]!

Висновок: Патерн (T[]) new Object[] припустимий тільки для суворо інкапсульованих внутрішніх структур (як у JDK ArrayList), які ніколи не повертають масив назовні у вигляді T[].

Виняток із правил: Масиви з Unbounded Wildcards (List<?>[])

Масиви дженериків з необмеженими підстановочними знаками (Unbounded Wildcards) створювати дозволено:

// ✅ Компільовано без помилок!
List<?>[] arrayOfLists = new List<?>[10];

Чому? Тип List<?> є опредметненим (Reifiable Type). Він не несе інформації про конкретний тип елементів, яку можна було б порушити через стирання типів. У такий масив не можна додати нічого, крім null (або через Object), тому типобезпека не порушується.

Generic Varargs та анотація @SafeVarargs

У Java синтаксис аргументів змінної довжини (Varargs) void foo(T... args) децукрується компілятором у масив: void foo(T[] args).

Це означає, що Java створює масив дженерик-типу при кожному виклику generic-методу з varargs!

public static <T> List<T> asList(T... elements) { /* ... */ }

Щоб запобігти попередженням компілятора про потенційне Heap Pollution, в Java 7 була введена анотація @SafeVarargs.

Розробник позначає метод @SafeVarargs, гарантуючи компілятору дотримання двох правил:

  1. Метод нічого не записує в масив elements (не змінює його).
  2. Метод не передає посилання на масив elements назовні в інший код.

4 Tricky Questions

1. Якщо створення масивів дженериків (new List<String>[10]) заборонено, чому компілятор дозволяє оголосити метод з generic varargs (void process(List<String>... lists))?

Відповідь: Це прагматичний компроміс творців Java. Синтаксис varargs був представлений у Java 5 одночасно з дженериками. Якби generic varargs були заборонені, стало б неможливо викликати стандартні методи на кшталт Arrays.asList(T... a) або фабрики колекцій List.of(E... elements).

Компілятор дозволяє цей синтаксис, але виводить суворе попередження: «Possible heap pollution from parameterized vararg type». Починаючи з Java 7, автор методу може гарантувати безпеку методу анотацією @SafeVarargs.

2. У чому принципова різниця між (T[]) new Object[size] та (T[]) Array.newInstance(clazz, size)?

Відповідь: Різниця полягає в реальному типі об’єкта масиву в купі (Runtime Type):

  • (T[]) new Object[size] створює фізичний Object[]. Компілятор пригнічує перевірку кастом (T[]), але в пам’яті JVM це залишається масив об’єктів. Якщо повернути його назовні як T[], клієнтський код впаде з ClassCastException при спробі привести його до String[].
  • (T[]) Array.newInstance(clazz, size) приймає токен класу (Class<T>) і створює в купі масив фактичного типу T (наприклад, new String[size]). Такий масив повністю безпечний і може віддаватися назовні клієнтам без ризику ClassCastException.

3. Чи дозволено створювати масив з wildcard: new List<?>[10]? Чому?

Відповідь: Так, створення масиву new List<?>[10] повністю дозволено компілятором.

Заборона на створення масивів дженериків поширюється лише на неопредметнені (non-reifiable) типи, у яких в рантаймі стирається конкретний параметр (наприклад, List<String> стирається до List). Тип List<?> є опредметненим (reifiable), оскільки підстановочний знак ? означає відсутність конкретних обмежень. При записі в List<?> компілятор дозволяє класти лише null, що виключає ризик Heap Pollution.

4. Чому метод List.toArray() повертає Object[], а для отримання типізованого масиву доводиться викликати list.toArray(new String[0]) або list.toArray(String[]::new)?

Відповідь: Через стирання типів (Type Erasure) екземпляр ArrayList під час виконання програми не має інформації про параметр E. Він не знає, викликано toArray() на List<String> чи List<Integer>, тому метод без параметрів не може використати Array.newInstance і змушений повертати плаский new Object[size].

Щоб повернути типізований масив, JVM необхідно передати токен типу або фабрику. Передача масиву new String[0] повідомляє методу точний runtime-тип через array.getClass().getComponentType(). Починаючи з Java 11, ідіоматичним стандартом став виклик list.toArray(String[]::new) через генератор масиву.


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

Головне про масиви та дженерики

  • Правило: new T[] та new List<String>[] заборонені (помилка: generic array creation).
  • Причина: Масиви — reified (знають тип у runtime, коваріантні); Дженерики — non-reified (стираються до Object, інваріантні). Їх перетин породжує ClassCastException.
  • Як обійти:
    1. List<T> замість масиву (найкращий вибір).
    2. Object[] з кастом всередині закритого класу (ArrayList). Не можна віддавати назовні!
    3. Array.newInstance(Class<T>, size) — безпечно створювати та віддавати назовні.
  • Винятки: new List<?>[size] дозволений, оскільки List<?> опредметнений.
  • Varargs: T... створює дженерик-масив під капотом. Для безпечних методів використовуйте @SafeVarargs.
  • Java 11: Використовуйте list.toArray(String[]::new) замість застарілих конструкцій.

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