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 ,其中包含 result 。 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;
}
该方法看起来很紧凑。此外,整个方法只创建了一个新的数组对象: 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、 LongStream 和 DoubleStream 。
但是, 只有 int 、 long 和 double 具有其 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 上获取。