Java多维数组揭秘!支持锯齿状结构,anewarray
和multi-anewarray
字节码玩转JVM。性能优化需注意循环顺序,避免频繁切换数组引用。维度已知时,多维数组是你的得力助手!
译自:Taking Java Arrays to Another Dimension
作者:Simon Ritter
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}};
此数组实现为数组引用的数组,如图所示。
实际上,有三个数组:一个用于保存值 1 和 2,一个用于保存值 3 和 4,一个用于保存对这两个数组的引用。由于数组引用是独立的,因此我们不需要使它们的大小相同。
我们可以更改第二个数组以保存三个值:
int[][] aiv = {{1, 2}, {3, 4, 5}};
数组存储现在看起来像这样:
如果我们有一个三维数组,则第二维中的每个元素都将成为一个数组引用。例如:
int aiv[][][] = {{{1},{2,3}},{{4,5},{6,7},{8,9}}};
数组存储将如下所示:
如果我们打印出值 aiv[1][0][1],我们将得到 5。
如果我们深入研究并查看 JVM 如何处理数组创建,我们会发现使用了三个字节码,具体取决于我们拥有的数组类型。
要创建基本类型的数组,可以使用 anewarray
字节码。它接受一个参数,指示数组将存储的基本类型。新数组的每个元素都初始化为数组类型的元素类型的默认初始值。这就是为什么当我们请求未显式初始化的基本类型的局部变量数组中的值时,不会出现编译器错误的原因。
Anewarray 字节码用于创建一维对象引用数组。它接受一个参数,该参数是运行时常量池的索引,用于定义数组将保存的对象类型。此字节码也可用于创建多维数组的第一个维度。同样,除非使用数组初始化代码,否则所有元素都将包含 null。
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 语言中的一个基本特性,在编译时已知维度大小时非常有用。希望您现在更好地了解它们的工作原理以及如何在代码中有效地使用它们。