Blog chevron_right 未分类

将 Java 数组提升到另一个维度

Taking Java Arrays to Another Dimension

通过本指南深入探索 Java 多维数组的复杂性。学习如何声明、初始化和使用 Java 数组来有效地管理复杂的数据结构。在这篇博文中,Azul 副首席技术官 Simon Ritter 解释了如何利用多维数组作为一种工具来实现更多功能。

Java 以及许多其他编程语言都包含数组的概念。数组是包含多个变量的对象。由于数组本身就是一个对象,因此数组中的变量也可以是数组,这就引出了多维数组的概念。

什么是 Java 中的多维数组?

在 Java 中有多种定义和填充多维数组的方法。

声明多维数组

首先,您可以声明一个数组变量,例如:

int[][] ai;

int aai[][];

如您所见,方括号(用于指示数组)的位置可以放在数组类型之后,也可以放在变量名之后。我倾向于将方括号放在数组类型之后,以便所有类型信息都集中在一个位置。

混合方括号位置

也可以混合以下位置:

int[] ai[];

不建议这样做,因为这会使数组的结构乍一看更难理解。以下示例非常清楚地说明了这一点:

int[][] x[][], y[][][], z[];

这等效于(但并不明显)单独的定义:

int[][][][] x;

int[][][][][] y;

int[][][] z;

在这些示例中,我们只是声明了可用于引用数组的变量,但并未创建数组。局部变量必须进行明确赋值;如果声明局部变量,则必须将其值设置为某个值。如果使用这些局部变量时没有将它们赋值给数组,编译器会报告 x、y 和 z 可能尚未初始化。

创建多维数组

创建多维数组的方法有两种(与创建单维数组的方法类似)。

第一种是使用数组初始化器。例如:

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

这里,我们定义了一个二维数组,并将值 1 和 2 赋给第一个数组,将值 3 和 4 赋给第二个数组。我特意在第二组括号后添加了逗号,因为这是有效的语法,即使没有第三组值(逗号是可选的)。数组的维度由编译器根据指定的值确定。在本例中,将创建一个 2×2 的数组。

第二种方法是实例化具有明确维度的数组:

int[][] aie = new int[2][2];

同样,我们有一个 2×2 的数组,但没有在其中写入具体的值。

数组中的默认值

我们还必须记住,在 Java 中,数组是一个对象,这就是我们使用 new 运算符的原因。为什么这很重要?作为局部变量,我们已经知道,如果不为数组引用赋值,代码将无法通过编译。现在,我们有了一个引用,但是如果我们尝试打印出第一个数组的第一个元素会发生什么?因为我们已经实例化了新的数组对象,所以 aie[0][0] 的值将为 0(我们稍后会解释原因)。如果我们有一个二维字符串数组,该值将为 null。即使它是一个局部变量,我们实例化的数组中也存储了默认值。

理解交错数组

关于 Java 中的多维数组,需要理解的关键一点是它们可能是不规则的(或者说是交错的,取决于描述它们的人)。这与 C 语言不同(Java 的语法主要基于 C 语言),C 语言具有矩形数组。

让我们看看这对开发人员来说意味着什么。

我们将重用之前的一个示例:

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

此数组实现为一个引用多个数组的数组,如图所示。

DIAGRAM: an array implemented as an array of references to arrays.

实际上,有三个数组:一个用于保存值 1 和 2,一个用于保存值 3 和 4,还有一个用于保存对这两个数组的引用。由于数组引用是独立的,因此我们不需要使它们的大小相同。

我们可以将第二个数组更改为保存三个值:

int[][] aiv = {{1, 2}, {3, 4, 5}};

数组存储现在如下所示:

DIAGRAM: an array storage holding three values.

如果我们有一个三维数组,则第二维中的每个元素都将成为数组引用。例如:

int aiv[][][] = {{{1},{2,3}},{{4,5},{6,7},{8,9}}};

数组存储将如下所示:

如果我们打印出值 aiv[1][0][1],我们将得到 5。

理解用于创建数组的 JVM 字节码

如果我们深入研究 JVM 如何处理数组创建,我们会发现根据我们拥有的数组类型,使用了三种字节码。

用于原始类型数组的字节码

  1. 要创建原始类型数组,需要使用 anewarray 字节码。它接受一个参数,该参数指示数组将存储的原始类型。新数组的每个元素都将初始化为该数组类型元素类型的默认初始值。这就是为什么在请求未显式初始化的原始类型局部变量数组中的值时不会出现编译器错误。

用于对象数组的字节码

  1. anewarray 字节码用于创建一维对象引用数组。它接受一个参数,该参数是运行时常量池的索引,该索引定义了数组将保存的对象类型。此字节码也可用于创建多维数组的第一维。同样,除非使用数组初始化代码,否则所有元素都将包含 null。

用于多维数组的字节码

  1. multi-anewarray 字节码可用于创建多维对象数组。与 anewarray 类似,它使用运行时常量池中的索引来确定数组将保存的对象类型(或为 null)。此外,它还包含数组维数的计数,以及一组表示数组每个维度大小的值。请注意,此字节码不用于原始类型的多维数组(因为常量池中没有这些数组的类型)。对于这些数组,将使用 anewarray 和 newarray 的组合来构造数组。对于不规则的对象数组,可以将 multianewarray 与 anewarray 组合使用,或者可以使用 anewarray 创建整个数组。javac 编译器将确定最高效的方法。

多维数组的性能考量

请谨慎使用多维数组,因为简单的更改可能会显著影响性能。例如,循环遍历二维数组:

int[][] aiv = {{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};

for (int x = 0; x < 3; x++)

for (int y = 0; y < 5; y++)

aiv[x][y] = aiv[x][y] + 1;

for (int y = 0; y < 5; y++)

for (int x = 0; x < 3; x++)

aiv[x][y] = aiv[x][y] + 1;

第一个循环版本比第二个版本效率高得多。这是因为我们之前看到的多维数组的结构。第二个版本需要不断在数组引用之间切换以访问各个元素,这会产生相关的开销。第一个版本维护一个数组的引用,并循环遍历其中存储的所有对象。

基准测试性能差异

我在 MacBook 上运行了类似的基准测试,使用了 2,000 个数组,每个数组包含 2,000 个元素,并重复循环一千次。第一个版本的循环完成耗时 620 毫秒,而第二个版本则耗时 4,200 毫秒。这几乎慢了七倍。

总结

多维数组是 Java 语言的一项基本特性,在编译时已知维度大小的情况下非常有用。希望您现在对它们的工作原理以及如何在代码中有效地使用它们有了更好的理解。

prime-cta-banner

We Love to Talk About Java

We’re here to answer any questions about Azul products, Java, pricing, or anything else.

本文原载于 《The New Stack》