Перейти к основному содержимому

Создание универсального массива в Java

· 8 мин. чтения

1. Введение

Мы можем захотеть использовать массивы как часть классов или функций, которые поддерживают дженерики . Из-за того, как Java обрабатывает дженерики, это может быть сложно.

В этом руководстве мы поймем проблемы использования дженериков с массивами. Затем мы создадим пример универсального массива.

Мы также посмотрим, где Java API решил аналогичную проблему.

2. Рекомендации по использованию универсальных массивов

Важное различие между массивами и дженериками заключается в том, как они обеспечивают проверку типов. В частности, массивы хранят и проверяют информацию о типах во время выполнения. Обобщения, однако, проверяют наличие ошибок типов во время компиляции и не имеют информации о типах во время выполнения.

Синтаксис Java предполагает, что мы могли бы создать новый общий массив:

T[] elements = new T[size];

Но если бы мы попытались это сделать, то получили бы ошибку компиляции.

Чтобы понять почему, давайте рассмотрим следующее:

public <T> T[] getArray(int size) {
T[] genericArray = new T[size]; // suppose this is allowed
return genericArray;
}

Поскольку несвязанный общий тип T разрешается в Object, наш метод во время выполнения будет выглядеть так:

public Object[] getArray(int size) {
Object[] genericArray = new Object[size];
return genericArray;
}

Затем, если мы вызовем наш метод и сохраним результат в массиве строк :

String[] myArray = getArray(5);

Код будет компилироваться нормально, но во время выполнения произойдет сбой с ClassCastException . Это потому, что мы только что присвоили Object [ ] ссылке String[] . В частности, неявное приведение компилятором не сможет преобразовать Object[] в требуемый тип String[] .

Хотя мы не можем инициализировать универсальные массивы напрямую, все же можно выполнить эквивалентную операцию, если точный тип информации предоставляется вызывающим кодом.

3. Создание универсального массива

Для нашего примера рассмотрим ограниченную структуру данных стека MyStack , где емкость фиксирована до определенного размера. Кроме того, поскольку мы хотим, чтобы стек работал с любым типом, разумным выбором реализации будет общий массив.

Во-первых, давайте создадим поле для хранения элементов нашего стека, который представляет собой общий массив типа E :

private E[] elements;

Во-вторых, добавим конструктор:

public MyStack(Class<E> clazz, int capacity) {
elements = (E[]) Array.newInstance(clazz, capacity);
}

Обратите внимание, как мы используем java.lang.reflect.Array#newInstance для инициализации нашего универсального массива , который требует двух параметров. Первый параметр указывает тип объекта внутри нового массива. Второй параметр указывает, сколько места нужно создать для массива. Поскольку результат Array#newInstance имеет тип Object , нам нужно привести его к E[] , чтобы создать наш универсальный массив.

Следует также отметить соглашение об именовании параметра типа clazz , а не class, что является зарезервированным словом в Java.

4. Рассмотрение ArrayList

4.1. Использование ArrayList вместо массива

Часто проще использовать универсальный ArrayList вместо универсального массива. Давайте посмотрим, как мы можем изменить MyStack для использования ArrayList .

Во-первых, давайте создадим поле для хранения наших элементов:

private List<E> elements;

Во-вторых, в конструкторе нашего стека мы можем инициализировать ArrayList начальной емкостью:

elements = new ArrayList<>(capacity);

Это делает наш класс проще, так как нам не нужно использовать отражение. Кроме того, мы не обязаны передавать литерал класса при создании нашего стека. Наконец, поскольку мы можем установить начальную емкость ArrayList , мы можем получить те же преимущества, что и массив.

Поэтому нам нужно создавать массивы дженериков только в редких случаях или когда мы взаимодействуем с какой-то внешней библиотекой, которой требуется массив.

4.2. Реализация списка массивов

Интересно, что сам ArrayList реализован с использованием универсальных массивов. Давайте заглянем внутрь ArrayList , чтобы узнать, как это сделать.

Во-первых, давайте посмотрим на поле элементов списка:

transient Object[] elementData;

Обратите внимание , что ArrayList использует Object в качестве типа элемента. Поскольку наш общий тип неизвестен до времени выполнения, Object используется как суперкласс любого типа.

Стоит отметить, что почти все операции в ArrayList могут использовать этот универсальный массив, поскольку им не нужно предоставлять строго типизированный массив внешнему миру, за исключением одного метода — toArray !

5. Создание массива из коллекции

5.1. Пример связанного списка

Давайте рассмотрим использование универсальных массивов в Java Collections API, где мы создадим новый массив из коллекции.

Во-первых, давайте создадим новый LinkedList с аргументом типа String и добавим в него элементы:

List<String> items = new LinkedList();
items.add("first item");
items.add("second item");

Во-вторых, давайте создадим массив только что добавленных элементов:

String[] itemsAsArray = items.toArray(new String[0]);

Чтобы построить наш массив, List . Для метода toArray требуется входной массив. Он использует этот массив исключительно для получения информации о типе для создания возвращаемого массива нужного типа.

В нашем примере выше мы использовали новую строку [0] в качестве входного массива для построения результирующего массива строк .

5.2. Реализация LinkedList.toArray

Давайте заглянем внутрь LinkedList.toArray , чтобы увидеть, как это реализовано в Java JDK.

Во-первых, давайте посмотрим на сигнатуру метода:

public <T> T[] toArray(T[] a)

Во-вторых, давайте посмотрим, как при необходимости создается новый массив:

a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);

Обратите внимание, как он использует Array#newInstance для создания нового массива, как в нашем примере со стеком ранее. Также обратите внимание, как параметр a используется для предоставления типа Array#newInstance. Наконец, результат Array#newInstance приводится к T[] для создания универсального массива.

6. Создание массивов из потоков

API Java Streams позволяет нам создавать массивы из элементов в потоке. Есть пара ловушек, на которые следует обратить внимание при создании массива правильного типа.

6.1. Использование toArray

Мы можем легко преобразовать элементы из потока Java 8 в массив:

Object[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.toArray();

assertThat(strings).containsExactly("A", "AAA", "AAB");

Однако следует отметить, что базовая функция toArray предоставляет нам массив Object , а не массив String :

assertThat(strings).isNotInstanceOf(String[].class);

Как мы видели ранее, точный тип каждого массива отличается. Поскольку тип в потоке является универсальным, библиотека не может определить тип во время выполнения .

6.2. Использование перегрузки toArray для получения типизированного массива

В то время как общие методы класса коллекций используют отражение для создания массива определенного типа, библиотека Java Streams использует функциональный подход. Мы можем передать лямбду или ссылку на метод, которая создает массив правильного размера и типа, когда Stream готов его заполнить:

String[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.toArray(String[]::new);

assertThat(strings).containsExactly("A", "AAA", "AAB");
assertThat(strings).isInstanceOf(String[].class);

Метод, который мы передаем, представляет собой IntFunction , который принимает целое число в качестве входных данных и возвращает новый массив этого размера. Это именно то, что делает конструктор String[] , поэтому мы можем использовать ссылку на методString[]::new .

6.3. Дженерики со своим собственным параметром типа

Теперь давайте представим, что мы хотим преобразовать значения в нашем потоке в объект, который сам имеет параметр типа, скажем, List или Optional . Возможно, у нас есть API, который мы хотим вызвать и который принимает в качестве входных данных Optional<String>[] .

Допустимо объявить такой массив:

Optional<String>[] strings = null;

Мы также можем легко взять наш Stream<String> и преобразовать его в Stream<Optional<String>> с помощью метода map :

Stream<Optional<String>> stream = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of);

Однако мы снова получили бы ошибку компилятора, если бы попытались построить наш массив:

// compiler error
Optional<String>[] strings = new Optional<String>[1];

К счастью, между этим примером и нашими предыдущими есть разница. Если String[] не является подклассом Object[] , Optional[] на самом деле является типом среды выполнения, идентичным Optional<String>[] . Другими словами, эту проблему мы можем решить с помощью приведения типов:

Stream<Optional<String>> stream = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of);
Optional<String>[] strings = stream
.toArray(Optional[]::new);

Этот код компилируется и работает, но выдает предупреждение о непроверенном назначении . Нам нужно добавить SuppressWarnings в наш метод, чтобы исправить это:

@SuppressWarnings("unchecked")

6.4. Использование вспомогательной функции

Если мы хотим избежать добавления SuppressWarnings в несколько мест нашего кода и хотим задокументировать способ создания нашего универсального массива из необработанного типа, мы можем написать вспомогательную функцию:

@SuppressWarnings("unchecked")
static <T, R extends T> IntFunction<R[]> genericArray(IntFunction<T[]> arrayCreator) {
return size -> (R[]) arrayCreator.apply(size);
}

Эта функция преобразует функцию создания массива необработанного типа в функцию, которая обещает создать массив определенного типа, который нам нужен:

Optional<String>[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
.filter(string -> string.startsWith("A"))
.map(Optional::of)
.toArray(genericArray(Optional[]::new));

Здесь нет необходимости подавлять предупреждение о непроверенном назначении.

Однако следует отметить, что эту функцию можно вызывать для приведения типов к более высоким типам. Например, если бы наш поток содержал объекты типа List<String> , мы могли бы неправильно вызвать genericArray для создания массива ArrayList<String> :

ArrayList<String>[] lists = Stream.of(singletonList("A"))
.toArray(genericArray(List[]::new));

Это скомпилируется, но вызовет исключение ClassCastException , поскольку ArrayList[] не является подклассом List[]. Однако для этого компилятор выдает предупреждение о непроверенном присваивании, так что это легко обнаружить.

7. Заключение

В этой статье мы сначала рассмотрели различия между массивами и дженериками, а затем привели пример создания универсального массива. Затем мы показали, что использование ArrayList может быть проще, чем использование универсального массива. Мы также рассмотрели использование универсального массива в API коллекций.

Наконец, мы увидели, как создавать массивы из Streams API и как создавать массивы типов, использующих параметр типа.

Как всегда, пример кода доступен на GitHub.