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

Операции с массивами в Java

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

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

Любой Java-разработчик знает, что создать чистое и эффективное решение при работе с массивами не всегда легко. Тем не менее, они являются центральным элементом экосистемы Java, и нам придется иметь с ними дело несколько раз.

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

2. Массивы и вспомогательные классы

Прежде чем продолжить, полезно понять, что такое массив в Java и как его использовать. Если вы впервые работаете с ним на Java, мы предлагаем взглянуть на этот предыдущий пост , где мы рассмотрели все основные концепции.

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

По этой причине для большинства наших операций мы будем использовать вспомогательные классы и методы: класс Arrays , предоставляемый Java, и класс ArrayUtils Apache .

Чтобы включить последний в наш проект, нам нужно добавить зависимость Apache Commons :

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>

Мы можем проверить последнюю версию этого артефакта на Maven Central .

3. Получить первый и последний элемент массива

Это одна из самых распространенных и простых задач благодаря доступу к массивам по индексу.

Начнем с объявления и инициализации массива int , который будет использоваться во всех наших примерах (если не указано иное):

int[] array = new int[] { 3, 5, 2, 5, 14, 4 };

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

int firstItem = array[0];
int lastItem = array[array.length - 1];

4. Получить случайное значение из массива

Используя объект java.util.Random , мы можем легко получить любое значение из нашего массива:

int anyValue = array[new Random().nextInt(array.length)];

5. Добавьте новый элемент в массив

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

Нам нужно начать с объявления нового массива большего размера и скопировать элементы базового массива во второй.

К счастью, класс Arrays предоставляет удобный метод для репликации значений массива в новую структуру другого размера:

int[] newArray = Arrays.copyOf(array, array.length + 1);
newArray[newArray.length - 1] = newItem;

При желании, если класс ArrayUtils доступен в нашем проекте, мы можем использовать его метод add (или его альтернативу addAll ) для достижения нашей цели в однострочном выражении:

int[] newArray = ArrayUtils.add(array, newItem);

Как мы можем себе представить, этот метод не изменяет исходный объект массива ; мы должны присвоить его выход новой переменной.

6. Вставьте значение между двумя значениями

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

Apache посчитал это типичным сценарием и реализовал метод в своем классе ArrayUtils для упрощения решения:

int[] largerArray = ArrayUtils.insert(2, array, 77);

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

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

7. Сравните два массива

Несмотря на то, что массивы являются Object и, следовательно, предоставляют метод equals , они используют его реализацию по умолчанию, полагаясь только на равенство ссылок.

В любом случае мы можем вызвать метод equals java.util.Arrays , чтобы проверить, содержат ли два объекта массива одинаковые значения: ``

boolean areEqual = Arrays.equals(array1, array2);

Примечание: этот метод не эффективен для зубчатых массивов . Подходящим методом проверки равенства многомерных структур является метод Arrays.deepEquals .

8. Проверьте, пуст ли массив

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

boolean isEmpty = array == null || array.length == 0;

Более того, у нас также есть null-safe метод во вспомогательном классе ArrayUtils , который мы можем использовать:

boolean isEmpty = ArrayUtils.isEmpty(array);

Эта функция по-прежнему зависит от длины структуры данных, которая также считает нули и пустые подмассивы допустимыми значениями, поэтому нам придется следить за этими пограничными случаями:

// These are empty arrays
Integer[] array1 = {};
Integer[] array2 = null;
Integer[] array3 = new Integer[0];

// All these will NOT be considered empty
Integer[] array3 = { null, null, null };
Integer[][] array4 = { {}, {}, {} };
Integer[] array5 = new Integer[3];

9. Как перетасовать элементы массива

Чтобы перетасовать элементы в массиве, мы можем использовать функцию ArrayUtil :

ArrayUtils.shuffle(array);

Это метод void , который работает с фактическими значениями массива.

10. Блокировка и распаковка массивов

Мы часто сталкиваемся с методами, которые поддерживают только массивы на основе Object .

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

Integer[] list = ArrayUtils.toObject(array);

Возможна и обратная операция:

Integer[] objectArray = { 3, 5, 2, 5, 14, 4 };
int[] array = ArrayUtils.toPrimitive(objectArray);

11. Удалить дубликаты из массива

Самый простой способ удалить дубликаты — преобразовать массив в реализацию Set .

Как мы знаем, Collections использует Generics и, следовательно, не поддерживает примитивные типы.

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

// Box
Integer[] list = ArrayUtils.toObject(array);
// Remove duplicates
Set<Integer> set = new HashSet<Integer>(Arrays.asList(list));
// Create array and unbox
return ArrayUtils.toPrimitive(set.toArray(new Integer[set.size()]));

Примечание: мы также можем использовать другие методы для преобразования между массивом и объектом Set .

Кроме того, если нам нужно сохранить порядок наших элементов, мы должны использовать другую реализацию Set , такую как LinkedHashSet .

12. Как распечатать массив

Как и в случае с методом equals , функция массива toString использует реализацию по умолчанию, предоставленную классом Object , что не очень полезно.

Классы Array и ArrayUtils поставляются со своими реализациями для преобразования структур данных в удобочитаемый String .

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

Класс Java Util предоставляет два статических метода, которые мы можем использовать:

  • toString : плохо работает с зубчатыми массивами
  • deepToString : поддерживает любые массивы на основе объектов , но не компилируется с аргументами примитивного массива .

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

String arrayAsString = ArrayUtils.toString(array);

13. Сопоставьте массив с другим типом

Часто бывает полезно применить операции ко всем элементам массива, возможно, преобразовав их в другой тип объекта.

Помня об этой цели, мы попытаемся создать гибкий вспомогательный метод, используя Generics:

public static <T, U> U[] mapObjectArray(
T[] array, Function<T, U> function,
Class<U> targetClazz) {
U[] newArray = (U[]) Array.newInstance(targetClazz, array.length);
for (int i = 0; i < array.length; i++) {
newArray[i] = function.apply(array[i]);
}
return newArray;
}

Если мы не используем Java 8 в нашем проекте, мы можем отказаться от аргумента Function и создать метод для каждого сопоставления, которое нам нужно выполнить.

Теперь мы можем повторно использовать наш универсальный метод для различных операций. Давайте создадим два тестовых случая, чтобы проиллюстрировать это:

@Test
public void whenMapArrayMultiplyingValues_thenReturnMultipliedArray() {
Integer[] multipliedExpectedArray = new Integer[] { 6, 10, 4, 10, 28, 8 };
Integer[] output =
MyHelperClass.mapObjectArray(array, value -> value * 2, Integer.class);

assertThat(output).containsExactly(multipliedExpectedArray);
}

@Test
public void whenMapDividingObjectArray_thenReturnMultipliedArray() {
Double[] multipliedExpectedArray = new Double[] { 1.5, 2.5, 1.0, 2.5, 7.0, 2.0 };
Double[] output =
MyHelperClass.mapObjectArray(array, value -> value / 2.0, Double.class);

assertThat(output).containsExactly(multipliedExpectedArray);
}

Для примитивных типов нам нужно сначала упаковать наши значения.

В качестве альтернативы мы можем обратиться к потокам Java 8, чтобы выполнить сопоставление для нас.

Сначала нам нужно преобразовать массив в Stream of Object s. Мы можем сделать это с помощью метода Arrays.stream .

Например, если мы хотим сопоставить наши значения int с пользовательским представлением String , мы реализуем это:

String[] stringArray = Arrays.stream(array)
.mapToObj(value -> String.format("Value: %s", value))
.toArray(String[]::new);

14. Фильтрация значений в массиве

Фильтрация значений из коллекции — обычная задача, которую нам, возможно, придется выполнять более одного раза.

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

Представьте, что мы хотим удалить все нечетные числа из массива:

int[] evenArray = Arrays.stream(array)
.filter(value -> value % 2 == 0)
.toArray();

15. Другие общие операции с массивами

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

Помимо показанных в этом руководстве, мы подробно рассмотрели другие операции в специальных сообщениях:

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

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

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

Как всегда, полный исходный код рабочих примеров доступен в нашем репозитории Github .