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"}

另外,我们不希望我们的方法仅适用于 字符串 数组,因此 我们将寻找通用解决方案

此外,我们不应该忘记原始数组的情况。如果我们的解决方案也适用于原始数组,那就太好了:

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

在上面的方法中,我们使用Java反射API创建一个通用数组实例: resultArray。

让我们编写一个测试来验证我们的方法是否有效:

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

如果我们执行测试,它就会通过。

这种方法非常简单。但是, 由于该方法接受 T[] 数组,因此它不支持连接原始数组

除此之外, 它的效率很低,因为它创建了一个 ArrayList 对象,稍后我们调用 toArray() 方法将其转换回 array 。在此过程中,Java List 对象增加了不必要的开销。

接下来,我们看看能否找到更有效的方法来解决问题。

4. 使用数组复制技术

Java 不提供数组连接方法,但它提供了两种数组复制方法: System.arraycopy()Arrays.copyOf()

我们可以使用Java的数组复制方法来解决这个问题。

这个想法是,我们创建一个新数组,例如 result ,其中包含 resultlength = 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;
}

该方法看起来很紧凑。此外,整个方法只创建了一个新的数组对象: 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 。此外, 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.concat() 方法连接两个 Stream 对象。

最后,我们返回一个包含串联 Stream 中所有元素的数组。

接下来,让我们构建一个简单的测试方法来检查解决方案是否有效:

@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、 LongStreamDoubleStream

但是, 只有 intlongdouble 具有其 Stream 类型 。也就是说,如果我们想要连接的原始数组的类型为 int[]long[]double[] ,我们可以选择正确的 Stream 类并调用 concat() 方法。

让我们看一个使用 IntStream 连接两个 int[] 数组的示例:

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[] – Guava: Ints.concat(int[] … 数组)
  • long[] – 番石榴: Longs.concat(long[] … 数组)
  • byte[] – Guava: Bytes.concat(byte[] … 数组)
  • double[] – 番石榴: Doubles.concat(double[] … arrays)

我们可以选择正确的原始实用程序类来连接原始数组。

接下来,让我们使用 Ints.concat() 方法连接两个 int[] 数组:

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

类似地,Guava 在上述方法内部使用 System.arraycopy() 来进行数组串联,以获得良好的性能。

八、结论

在本文中,我们通过示例介绍了在 Java 中连接两个数组的不同方法。

与往常一样,本文随附的完整代码示例可在 GitHub 上获取。