Рубрики
Без рубрики

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

Свободные типы дженериков Java трудно принудить к сильным типам массивов Java. Мы исследуем проблему и некоторые общие решения.

Автор оригинала: baeldung.

1. введение

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

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

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

2. Соображения При Использовании Универсальных Массивов

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

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

T[] elements = new T[size];

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

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

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

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

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

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

String[] myArray = getArray(5);

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

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

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

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

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

private E[] elements;

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

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

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

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

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

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

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

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

private List elements;

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

elements = new ArrayList<>(capacity);

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

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

4.2. Реализация ArrayList

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

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

transient Object[] elementData;

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

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

5. Построение массива из коллекции

5.1. Пример списка ссылок

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

Сначала давайте создадим новый Связанный список с аргументом типа Строка и добавим в него элементы:

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

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

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

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

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

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

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

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

public  T[] toArray(T[] a)

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

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

Обратите внимание, как он использует Массив#newInstance для создания нового массива, как в нашем примере стека ранее. Кроме того, обратите внимание, как параметр a используется для указания типа Массив#Новая установка. Наконец, результат из массива#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 предоставляет нам массив Объекта , а не массив Строки :

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

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

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

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

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);

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

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

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

Допустимо объявлять массив такого рода:

Optional[] strings = null;

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

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

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

// compiler error
Optional[] strings = new Optional[1];

К счастью, между этим примером и нашими предыдущими примерами есть разница. Где String[] не является подклассом Объекта[] , Необязательный[] фактически является типом среды выполнения, идентичным Необязательному[] . Другими словами, это проблема, которую мы можем решить с помощью приведения типов:

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

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

@SuppressWarnings("unchecked")

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

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

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

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

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

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

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

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

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

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

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

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

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