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 获取。


原始标题:Convert 2D Array Into 1D Array | Baeldung