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

Объединить два массива в Java

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

1. Обзор

В этом уроке мы собираемся обсудить, как объединить два массива в Java.

Во-первых, мы реализуем наши собственные методы с помощью стандартного Java API.

Затем мы рассмотрим, как решить проблему с помощью часто используемых библиотек.

2. Введение в проблему

Быстрые примеры могут ясно объяснить проблему.

Допустим, у нас есть два массива:

String[] strArray1 = {"element 1", "element 2", "element 3"};
String[] strArray2 = {"element 4", "element 5"};

Теперь мы хотим присоединиться к ним и получить новый массив:

String[] expectedStringArray = {"element 1", "element 2", "element 3", "element 4", "element 5"}

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

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

int[] intArray1 = { 0, 1, 2, 3 };
int[] intArray2 = { 4, 5, 6, 7 };
int[] expectedIntArray = { 0, 1, 2, 3, 4, 5, 6, 7 };

В этом руководстве мы рассмотрим различные подходы к решению проблемы.

3. Использование коллекций Java

Когда мы рассматриваем эту проблему, может появиться быстрое решение.

Что ж, в Java нет вспомогательного метода для объединения массивов. Однако, начиная с Java 5, служебный класс Collections представил метод addAll(Collection<? super T> c, T… elements) .

Мы можем создать объект List , а затем дважды вызвать этот метод, чтобы добавить два массива в список. Наконец, мы преобразуем полученный список обратно в массив:

static <T> T[] concatWithCollection(T[] array1, T[] array2) {
List<T> resultList = new ArrayList<>(array1.length + array2.length);
Collections.addAll(resultList, array1);
Collections.addAll(resultList, array2);

@SuppressWarnings("unchecked")
//the type cast is safe as the array1 has the type T[]
T[] resultArray = (T[]) Array.newInstance(array1.getClass().getComponentType(), 0);
return resultList.toArray(resultArray);
}

В приведенном выше методе мы используем API отражения Java для создания универсального экземпляра массива: resultArray.

Давайте напишем тест, чтобы проверить, работает ли наш метод:

@Test
public void givenTwoStringArrays_whenConcatWithList_thenGetExpectedResult() {
String[] result = ArrayConcatUtil.concatWithCollection(strArray1, strArray2);
assertThat(result).isEqualTo(expectedStringArray);
}

Если мы выполним тест, он пройдет.

Этот подход довольно прост. Однако, поскольку метод принимает массивы T[] , он не поддерживает объединение примитивных массивов .

Кроме того, он неэффективен, так как создает объект ArrayList , а позже мы вызываем метод toArray() , чтобы преобразовать его обратно в массив . В этой процедуре объект списка Java добавляет ненужные накладные расходы.

Далее, давайте посмотрим, сможем ли мы найти более эффективный способ решения проблемы.

4. Использование техники копирования массива

Java не предлагает метод объединения массивов, но предоставляет два метода копирования массивов: System.arraycopy() и Arrays.copyOf() .

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

Идея в том, что мы создаем новый массив, скажем, результат , который имеет результат . length = array1.length + array2.length и скопируйте элементы каждого массива в результирующий массив.

4.1. Непримитивные массивы

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

static <T> T[] concatWithArrayCopy(T[] array1, T[] array2) {
T[] result = Arrays.copyOf(array1, array1.length + array2.length);
System.arraycopy(array2, 0, result, array1.length, array2.length);
return result;
}

Метод выглядит компактно. Кроме того, весь метод создал только один новый объект массива: результат .

Теперь давайте напишем тестовый метод, чтобы проверить, работает ли он так, как мы ожидаем:

@Test
public void givenTwoStringArrays_whenConcatWithCopy_thenGetExpectedResult() {
String[] result = ArrayConcatUtil.concatWithArrayCopy(strArray1, strArray2);
assertThat(result).isEqualTo(expectedStringArray);
}

Тест пройдет, если мы его прогоним.

Нет ненужного создания объектов. Таким образом, этот метод более производительный, чем подход с использованием Java Collections .

С другой стороны, этот общий метод принимает параметры только с типом T[] . Поэтому мы не можем передавать в метод примитивные массивы.

Однако мы можем изменить метод, чтобы он поддерживал примитивные массивы.

Далее давайте подробнее рассмотрим, как добавить поддержку примитивных массивов.

4.2. Добавить поддержку примитивных массивов

Чтобы метод поддерживал примитивные массивы, нам нужно изменить тип параметров с T[] на T и выполнить некоторые проверки безопасности типов.

Во-первых, давайте взглянем на модифицированный метод:

static <T> T concatWithCopy2(T array1, T array2) {
if (!array1.getClass().isArray() || !array2.getClass().isArray()) {
throw new IllegalArgumentException("Only arrays are accepted.");
}

Class<?> compType1 = array1.getClass().getComponentType();
Class<?> compType2 = array2.getClass().getComponentType();

if (!compType1.equals(compType2)) {
throw new IllegalArgumentException("Two arrays have different types.");
}

int len1 = Array.getLength(array1);
int len2 = Array.getLength(array2);

@SuppressWarnings("unchecked")
//the cast is safe due to the previous checks
T result = (T) Array.newInstance(compType1, len1 + len2);

System.arraycopy(array1, 0, result, 0, len1);
System.arraycopy(array2, 0, result, len1, len2);

return result;
}

Очевидно, что метод concatWithCopy2() длиннее оригинальной версии. Но это не трудно понять. Теперь давайте быстро пройдемся по нему, чтобы понять, как он работает.

Поскольку метод теперь допускает параметры с типом T , нам нужно убедиться, что оба параметра являются массивами :

if (!array1.getClass().isArray() || !array2.getClass().isArray()) {
throw new IllegalArgumentException("Only arrays are accepted.");
}

Это все еще недостаточно безопасно, если два параметра являются массивами. Например, мы не хотим объединять массив Integer[] и массив String[] . Итак, нам нужно убедиться, что ComponentType двух массивов идентичен :

if (!compType1.equals(compType2)) {
throw new IllegalArgumentException("Two arrays have different types.");
}

После проверки безопасности типов мы можем создать универсальный экземпляр массива, используя объект ConponentType, и скопировать массивы параметров в результирующий массив. Он очень похож на предыдущий метод concatWithCopy() .

4.3. Тестирование метода concatWithCopy2()

Далее давайте проверим, работает ли наш новый метод так, как мы ожидали. Во-первых, мы передаем два объекта, не являющихся массивами, и смотрим, вызывает ли метод ожидаемое исключение:

@Test
public void givenTwoStrings_whenConcatWithCopy2_thenGetException() {
String exMsg = "Only arrays are accepted.";
try {
ArrayConcatUtil.concatWithCopy2("String Nr. 1", "String Nr. 2");
fail(String.format("IllegalArgumentException with message:'%s' should be thrown. But it didn't", exMsg));
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage(exMsg);
}
}

В приведенном выше тесте мы передаем методу два объекта String . Если мы выполняем тест, он проходит. Это означает, что мы получили ожидаемое исключение.

Наконец, давайте создадим тест, чтобы проверить, может ли новый метод объединять примитивные массивы:

@Test
public void givenTwoArrays_whenConcatWithCopy2_thenGetExpectedResult() {
String[] result = ArrayConcatUtil.concatWithCopy2(strArray1, strArray2);
assertThat(result).isEqualTo(expectedStringArray);

int[] intResult = ArrayConcatUtil.concatWithCopy2(intArray1, intArray2);
assertThat(intResult).isEqualTo(expectedIntArray);
}

На этот раз мы дважды вызвали метод concatWithCopy2() . Сначала мы передаем два массива String[] . Затем мы передаем два примитивных массива int[] .

Тест пройдет, если мы его запустим. Теперь мы можем сказать, что метод concatWithCopy2() работает так, как мы и ожидали.

5. Использование Java Stream API

Если версия Java, с которой мы работаем, — 8 или новее, доступен Stream API . Мы также можем решить проблему с помощью Stream API.

Во- первых, мы можем получить поток из массива с помощью метода Arrays.stream() . Кроме того, класс Stream предоставляет статический метод concat() для объединения двух объектов Stream .

Теперь давайте посмотрим, как объединить два массива с помощью Stream.

5.1. Объединение непримитивных массивов

Создание универсального решения с использованием Java Streams довольно просто:

static <T> T[] concatWithStream(T[] array1, T[] array2) {
return Stream.concat(Arrays.stream(array1), Arrays.stream(array2))
.toArray(size -> (T[]) Array.newInstance(array1.getClass().getComponentType(), size));
}

Сначала мы преобразуем два входных массива в объекты Stream . Во-вторых, мы объединяем два объекта Stream с помощью метода Stream.concat() .

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

Далее давайте создадим простой тестовый метод, чтобы проверить, работает ли решение:

@Test
public void givenTwoStringArrays_whenConcatWithStream_thenGetExpectedResult() {
String[] result = ArrayConcatUtil.concatWithStream(strArray1, strArray2);
assertThat(result).isEqualTo(expectedStringArray);
}

Тест пройдет, если мы передадим два массива String[] .

Наверное, мы заметили, что наш универсальный метод принимает параметры типа T[] . Поэтому он не будет работать для примитивных массивов .

Далее давайте посмотрим, как объединить два примитивных массива с помощью Java Streams.

5.2. Объединение примитивных массивов

Stream API предоставляет различные классы Stream , которые могут преобразовывать объект Stream в соответствующий примитивный массив, например IntStream , LongStream и DoubleStream .

Однако только int , long и double имеют свои типы Stream . То есть, если примитивные массивы, которые мы хотим объединить, имеют тип int[] , long[] или double[] , мы можем выбрать правильный класс Stream и вызвать метод concat() .

Давайте посмотрим на пример объединения двух массивов int[] с помощью IntStream :

static int[] concatIntArraysWithIntStream(int[] array1, int[] array2) {
return IntStream.concat(Arrays.stream(array1), Arrays.stream(array2)).toArray();
}

Как показано выше, метод Arrays.stream(int[]) возвращает объект IntStream .

Кроме того, метод IntStream.toArray() возвращает int[] . Поэтому нам не нужно заботиться о преобразованиях типов.

Как обычно, давайте создадим тест, чтобы увидеть, работает ли он с нашими входными данными int[] :

@Test
public void givenTwoIntArrays_whenConcatWithIntStream_thenGetExpectedResult() {
int[] intResult = ArrayConcatUtil.concatIntArraysWithIntStream(intArray1, intArray2);
assertThat(intResult).isEqualTo(expectedIntArray);
}

Если мы проведем тест, он пройдет.

6. Использование библиотеки Apache Commons Lang

Библиотека Apache Commons Lang широко используется в приложениях Java в реальном мире.

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

Класс ArrayUtils предоставляет ряд методов addAll() , которые поддерживают объединение как непримитивных, так и примитивных массивов.

Проверим это тестовым методом:

@Test
public void givenTwoArrays_whenConcatWithCommonsLang_thenGetExpectedResult() {
String[] result = ArrayUtils.addAll(strArray1, strArray2);
assertThat(result).isEqualTo(expectedStringArray);

int[] intResult = ArrayUtils.addAll(intArray1, intArray2);
assertThat(intResult).isEqualTo(expectedIntArray);
}

Внутри методы ArrayUtils.addAll() используют производительный метод System.arraycopy() для объединения массивов.

7. Использование библиотеки гуавы

Подобно библиотеке Apache Commons, Guava — еще одна библиотека, любимая многими разработчиками.

Guava также предоставляет удобные вспомогательные классы для объединения массивов.

Если мы хотим объединить непримитивные массивы, хорошим выбором будет метод ObjectArrays.concat() :

@Test
public void givenTwoStringArrays_whenConcatWithGuava_thenGetExpectedResult() {
String[] result = ObjectArrays.concat(strArray1, strArray2, String.class);
assertThat(result).isEqualTo(expectedStringArray);
}

Guava предлагает примитивные утилиты для каждого примитива. Все примитивные утилиты предоставляют `метод concat()` для объединения массивов с соответствующими типами, например:

  • int[] – Гуава: Ints.concat(int[] … массивы)
  • long[] – Гуава: Longs.concat(long[] … массивы)
  • byte[] – Guava: Bytes.concat(byte[] … массивы)
  • double[] — Guava: Doubles.concat(double[] … массивы)

Мы можем просто выбрать правильный примитивный служебный класс для конкатенации примитивных массивов.

Далее давайте объединим наши два массива int[] с помощью метода Ints.concat() :

@Test
public void givenTwoIntArrays_whenConcatWithGuava_thenGetExpectedResult() {
int[] intResult = Ints.concat(intArray1, intArray2);
assertThat(intResult).isEqualTo(expectedIntArray);
}

Точно так же Guava внутренне использует System.arraycopy() в вышеупомянутых методах для объединения массивов для повышения производительности.

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

В этой статье мы рассмотрели различные подходы к объединению двух массивов в Java на примерах.

Как обычно, полные примеры кода, сопровождающие эту статью, доступны на GitHub.