Чи можна створити масив дженерик-типу
Спроба написати new T[10] або new List
🟢 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 побудовані на взаємовиключних принципах:
- Масиви опредметнені (Reified): вони знають свій точний тип елементів під час виконання програми (в рантаймі) і перевіряють кожен записуваний елемент (
ArrayStoreException). - Дженерики стираються (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, гарантуючи компілятору дотримання двох правил:
- Метод нічого не записує в масив
elements(не змінює його). - Метод не передає посилання на масив
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. - Як обійти:
List<T>замість масиву (найкращий вибір).Object[]з кастом всередині закритого класу (ArrayList). Не можна віддавати назовні!Array.newInstance(Class<T>, size)— безпечно створювати та віддавати назовні.
- Винятки:
new List<?>[size]дозволений, оскількиList<?>опредметнений. - Varargs:
T...створює дженерик-масив під капотом. Для безпечних методів використовуйте@SafeVarargs. - Java 11: Використовуйте
list.toArray(String[]::new)замість застарілих конструкцій.