跳到内容

第 1 卷 / 第 6 章 / 第 4 课

6.4 多维数组与矩形校验 ​

前置知识:一维数组、循环和方法契约

预计时间:25 分钟

完成标志:能安全遍历矩形、空行、不等长行和 null 行,并能在矩阵算法前验证形状

一维数组按单个索引定位。棋盘、表格和像素网格还需要行、列两个坐标。Java 用数组的数组表达这种结构。

1. int[][] 的组件类型是 int[] ​

java
int[][] grid = new int[3][4];

这会创建一个长度为 3 的外层数组,并为每个组件创建一个长度为 4 的 int[] 行。所有整数组件先取默认值 0。

java
int[][] grid = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

System.out.println(grid[1][2]); // 7

表达式分两步理解:grid[1] 得到第二个 int[],再由 [2] 读取该行第三个整数。

Java 规定的是数组对象和索引语义。程序不应根据源码中的 int[3][4] 假设所有行在物理内存中连续排列。

2. 遍历时使用当前行的长度 ​

java
for (int row = 0; row < grid.length; row++) {
    for (int column = 0; column < grid[row].length; column++) {
        System.out.print(grid[row][column] + " ");
    }
    System.out.println();
}

grid.length 是外层数组长度;grid[row].length 是当前行长度。即使当前示例是矩形,也不要把内层条件写死成 grid[0].length,否则换成不规则数组后容易越界。

只读时可以使用增强 for:

java
for (int[] row : grid) {
    for (int value : row) {
        System.out.print(value + " ");
    }
    System.out.println();
}

3. 外层数组可以分阶段创建 ​

java
int[][] seats = new int[3][];
System.out.println(seats[0]); // null

seats[0] = new int[4];
seats[1] = new int[2];
seats[2] = new int[5];

new int[3][] 只创建三个行引用组件,它们最初都是 null。每行可在之后创建成不同长度,这叫不规则数组或锯齿数组。

java
int[][] triangle = {
    {},
    {1},
    {2, 3},
    null
};

若输入允许 null 行,遍历必须先处理:

java
static long sumRows(int[][] values) {
    long total = 0L;
    for (int[] row : values) {
        if (row == null) {
            continue;
        }
        for (int value : row) {
            total = Math.addExact(total, value);
        }
    }
    return total;
}

也可以把 null 行视为非法输入并抛异常。关键是写进契约,不要让算法偶然决定。

4. 需要矩形时先验证形状 ​

矩阵转置、矩阵乘法等算法通常要求每行长度相同且不为 null。验证函数可以返回列数:

java
static int requireRectangular(int[][] matrix) {
    if (matrix == null) {
        throw new NullPointerException("matrix");
    }
    if (matrix.length == 0) {
        return 0;
    }
    if (matrix[0] == null) {
        throw new IllegalArgumentException("null row: 0");
    }
    int columns = matrix[0].length;
    for (int row = 1; row < matrix.length; row++) {
        if (matrix[row] == null || matrix[row].length != columns) {
            throw new IllegalArgumentException("not rectangular at row " + row);
        }
    }
    return columns;
}

空的外层数组没有第一行,因此本文把它视为 0 列。其他 API 也可以要求调用者额外提供列数,只要契约一致。

验证后再转置:

java
static int[][] transpose(int[][] matrix) {
    int columns = requireRectangular(matrix);
    int[][] result = new int[columns][matrix.length];
    for (int row = 0; row < matrix.length; row++) {
        for (int column = 0; column < columns; column++) {
            result[column][row] = matrix[row][column];
        }
    }
    return result;
}

输入为 3×4 时结果为 4×3。输入为 0×0 时结果仍为空数组。1×0 转置后也得到空外层数组,而这个结构无法保存“有 1 列”的信息;再次转置不会恢复原来的那一个空行。需要完整保留零维形状时,应把行列数另存为元数据。

5. 嵌套数组的输出与比较 ​

java
import java.util.Arrays;

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

System.out.println(Arrays.deepToString(a));
// [[1, 2], [3, 4]]
System.out.println(Arrays.deepEquals(a, b));
// true

Arrays.toString(a) 只格式化外层组件,因此每行仍显示为数组对象字符串。嵌套数组要使用 deepToString;按嵌套内容比较则使用 deepEquals。自引用对象数组需要分别看契约:deepToString 用 [...] 标记循环引用,而 deepEquals 对直接或间接包含自身的数组不定义结果,不能当作通用图比较器。

常见故障 ​

现象原因修复方向
grid[row].length 空指针当前行是 null拒绝或跳过 null 行
某一行越界假设所有行等长使用当前行长度或先验证矩形
转置结果行列颠倒目标索引写反目标写 result[column][row]
输出出现 [I@...只格式化了外层使用 Arrays.deepToString
Arrays.equals 返回 false它比较外层组件引用嵌套内容用 deepEquals

离开这一页前 ​

让遍历函数依次接收矩形数组、空外层数组、空行、不等长行和 null 行,明确每种输入是接受、跳过还是拒绝。转置前先验证形状,输出与比较嵌套内容时再区分 toString、deepToString、equals 与 deepEquals。

→ 下一步:数组练习与排障

Built with VitePress | Software Systems Atlas