1. 概述
数组是任何语言中最基础的数据结构。虽然多数情况下我们不直接操作它们,但掌握高效操作数组的技巧能显著提升代码质量。
本教程将学习如何将二维数组"展平"为一维数组。例如,将 { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }
转换为 {1, 2, 3, 4, 5, 6, 7, 8, 9}
。
虽然我们以二维数组为例,但这些思路可扩展到任意维度的数组。本文使用基本类型 int
数组演示,但原理适用于任何数组类型。
2. 循环与基本类型数组
最直接的方案是使用 for
循环逐个转移元素。但为提升性能,必须先计算元素总数来创建目标数组。
当所有子数组长度相同时,计算很简单。但若处理锯齿数组(jagged array),就需要遍历每个子数组:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithForLoopAndTotalNumberOfElements_thenGetCorrectResult(
int [][] initialArray, int[] expected) {
int totalNumberOfElements = 0;
for (int[] numbers : initialArray) {
totalNumberOfElements += numbers.length;
}
int[] actual = new int[totalNumberOfElements];
int position = 0;
for (int[] numbers : initialArray) {
for (int number : numbers) {
actual[position] = number;
++position;
}
}
assertThat(actual).isEqualTo(expected);
}
还可以优化:在第二个循环中使用 System.arraycopy()
:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithArrayCopyAndTotalNumberOfElements_thenGetCorrectResult(
int [][] initialArray, int[] expected) {
int totalNumberOfElements = 0;
for (int[] numbers : initialArray) {
totalNumberOfElements += numbers.length;
}
int[] actual = new int[totalNumberOfElements];
int position = 0;
for (int[] numbers : initialArray) {
System.arraycopy(numbers, 0, actual, position, numbers.length);
position += numbers.length;
}
assertThat(actual).isEqualTo(expected);
}
✅ System.arraycopy()
性能优异,是数组复制的推荐方案(与 clone()
并列)。⚠️ 但操作引用类型数组时需注意:它们执行的是浅拷贝(shallow copy)。
技术上,可以跳过首次循环的元素计数,改为动态扩容:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithArrayCopy_thenGetCorrectResult(
int [][] initialArray, int[] expected) {
int[] actual = new int[]{};
int position = 0;
for (int[] numbers : initialArray) {
if (actual.length < position + numbers.length) {
int[] newArray = new int[actual.length + numbers.length];
System.arraycopy(actual, 0, newArray, 0, actual.length );
actual = newArray;
}
System.arraycopy(numbers, 0, actual, position, numbers.length);
position += numbers.length;
}
assertThat(actual).isEqualTo(expected);
}
❌ 但此方案会严重拖累性能,将时间复杂度从 O(n) 恶化到 O(n²)。 应避免使用,或采用类似 ArrayList
的优化扩容算法(如均摊分析)。
3. 列表(List)
Java 集合 API 提供了更便捷的元素管理方式。若用 List
作为返回类型或中间容器,代码会简化:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithForLoopAndAdditionalList_thenGetCorrectResult(
int [][] initialArray, int[] intArray) {
List<Integer> expected = Arrays.stream(intArray).boxed().collect(Collectors.toList());
List<Integer> actual = new ArrayList<>();
for (int[] numbers : initialArray) {
for (int number : numbers) {
actual.add(number);
}
}
assertThat(actual).isEqualTo(expected);
}
此时无需手动处理数组扩容,List
会自动完成。还可将子数组转为 List
后利用 addAll()
:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithForLoopAndLists_thenGetCorrectResult(
int [][] initialArray, int[] intArray) {
List<Integer> expected = Arrays.stream(intArray).boxed().collect(Collectors.toList());
List<Integer> actual = new ArrayList<>();
for (int[] numbers : initialArray) {
List<Integer> listOfNumbers = Arrays.stream(numbers).boxed().collect(Collectors.toList());
actual.addAll(listOfNumbers);
}
assertThat(actual).isEqualTo(expected);
}
⚠️ 集合不支持基本类型,装箱(boxing)会产生显著开销。 当元素量大或性能敏感时,应避免使用包装类。
4. Stream API
这类常见问题,Stream API 提供了更优雅的解决方案:
@ParameterizedTest
@MethodSource("arrayProvider")
void giveTwoDimensionalArray_whenFlatWithStream_thenGetCorrectResult(
int [][] initialArray, int[] expected) {
int[] actual = Arrays.stream(initialArray).flatMapToInt(Arrays::stream).toArray();
assertThat(actual).containsExactly(expected);
}
我们使用 flatMapToInt()
是因为处理基本类型数组。引用类型对应方案是 flatMap()
。 这是最简洁易读的方案,但需理解 Stream API。
5. 总结
我们通常不直接操作数组,但作为最基础的数据结构,掌握其操作技巧至关重要。
System
类、集合框架和 Stream API 提供了丰富的数组操作方法。但务必权衡各方案的利弊,根据实际场景选择最合适的实现。
完整代码可在 GitHub 获取。