Как создать generic-массив в Java? Полный разбор с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Generic array creation» — ошибка, которая бесит каждого Java-разработчика

Вы когда-нибудь писали что-то вроде new T[10] и получали в ответ красную волну от компилятора? Если да — вы не одиноки. Вопрос «How can I create a generic array in Java?» на Stack Overflow собрал сотни тысяч просмотров. И это не случайно: создание generic-массива в Java — одна из тех тем, где теория расходится с практикой, а IDE отказывается понимать, что вы от неё хотите.

В этой статье мы разберём, почему Java запрещает generic-массивы, как обойти ограничение без потери типобезопасности, и когда массив всё-таки лучше ArrayList. Поехали!

Почему new T[10] не работает? Корень зла — стирание типов

Чтобы понять, как создать generic-массив в Java, нужно сначала осознать, почему компилятор кричит. Дело в type erasure (стирании типов). Java-дженерики существуют только на этапе компиляции. Во время выполнения информация о типе T стирается, и на её место подставляется Object (или граница, если она задана).

Массивы же в Java — это reifiable типы. Они знают свой тип компонента во время выполнения. Поэтому JVM может проверить, что вы кладёте в массив только элементы правильного типа. Если бы вы могли написать new T[10], то во время выполнения массив был бы Object[], и вы могли бы положить туда String, Integer и Date одновременно — что нарушает всю идею типобезопасности.

Именно поэтому компилятор выдаёт ошибку:

// ❌ Ошибка компиляции: generic array creation
public class Box<T> {
    private T[] array = new T[10];  // Cannot create a generic array of T
}

Способ 1: Самый простой — используйте ArrayList

Чаще всего вам вообще не нужен массив. ArrayList внутри работает с Object[], но предоставляет типобезопасный интерфейс. Это идеальное решение, если вы не зациклены на производительности на уровне наносекунд.

import java.util.ArrayList;
import java.util.List;

public class GenericListExample<T> {
    private List<T> list = new ArrayList<>();

    public void add(T item) {
        list.add(item);
    }

    public T get(int index) {
        return list.get(index);
    }

    public int size() {
        return list.size();
    }
}

Плюсы: полная типобезопасность, никаких предупреждений, поддержка динамического расширения.
Минусы: небольшой оверхед на упаковку/распаковку примитивов (но для 99% приложений это незаметно).

Способ 2: Классический хак с (T[]) new Object[size]

Если вам действительно нужен массив (например, для производительности или совместимости с legacy API), можно использовать приведение типов. Это безопасно, если вы гарантируете, что массив никогда не покинет пределов вашего класса.

public class Stack<T> {
    private T[] elements;
    private int size = 0;
    private static final int DEFAULT_CAPACITY = 10;

    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (T[]) new Object[DEFAULT_CAPACITY];
    }

    public void push(T item) {
        if (size == elements.length) {
            resize();
        }
        elements[size++] = item;
    }

    public T pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        T result = elements[--size];
        elements[size] = null;  // Помогаем GC
        return result;
    }

    private void resize() {
        elements = Arrays.copyOf(elements, elements.length * 2);
    }
}

Важно: аннотация @SuppressWarnings("unchecked") — это не костыль, а осознанное решение. Вы берёте на себя ответственность за типобезопасность. Пока массив elements остаётся приватным и вы не возвращаете его наружу (кроме как через toArray() с правильной логикой), всё в порядке.

Когда это может сломаться?

Если вы передадите массив наружу:

// ❌ Опасно!
public T[] getArray() {
    return elements;  // Вернёт Object[]! Код упадёт с ClassCastException
}

Способ 3: Использовать Array.newInstance() — для продвинутых

Если вам нужно создать generic-массив конкретного типа (а не Object[]), используйте рефлексию. Метод Array.newInstance(Class, int) создаёт массив нужного типа во время выполнения.

import java.lang.reflect.Array;

public class GenericArrayFactory<T> {
    private Class<T> type;

    public GenericArrayFactory(Class<T> type) {
        this.type = type;
    }

    @SuppressWarnings("unchecked")
    public T[] createArray(int size) {
        return (T[]) Array.newInstance(type, size);
    }
}

// Использование:
GenericArrayFactory<String> factory = new GenericArrayFactory<>(String.class);
String[] strings = factory.createArray(5);  // ✅ Работает!

Минусы: нужно передавать Class в конструктор, что делает код громоздким. Плюс рефлексия работает медленнее.

Способ 4: Generic-массив через Supplier (Java 8+)

Современный подход — передавать не класс, а ссылку на конструктор массива:

import java.util.function.IntFunction;

public class ArrayBuilder<T> {
    private IntFunction<T[]> arrayConstructor;

    public ArrayBuilder(IntFunction<T[]> arrayConstructor) {
        this.arrayConstructor = arrayConstructor;
    }

    public T[] build(int size) {
        return arrayConstructor.apply(size);
    }
}

// Использование:
ArrayBuilder<Integer> builder = new ArrayBuilder<>(Integer[]::new);
Integer[] numbers = builder.build(10);  // ✅

Этот способ особенно удобен в связке со Stream API:

List<String> list = List.of("a", "b", "c");
String[] array = list.toArray(String[]::new);  // ✅ Идеально!

Когда массив лучше ArrayList?

Давайте будем честными: в 90% случаев ArrayList — это правильный выбор. Но есть сценарии, где массив выигрывает: