📋 Раздел 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<?> является реифицируемым, так как подстановочный знак ? означает отсутствие конкретных ограничений. При записи в 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) вместо устаревших конструкций.

Связанные темы