为什么Java没有真正的多维数组?问题背景为什么Java会这样做这有什么问题对此可以做些什么问题更新

对于那些不想要背景的人来说,TL;DR版本是以下具体问题:

问题

为什么Java没有真正的多维数组的实现?是否有坚实的技术原因?我在这里错过了什么?

背景

Java在语法级别具有多维数组,因为可以声明

int[][] arr = new int[10][10];

但似乎这真的不是人们所期望的。它不是让JVM分配一个足够大的连续RAM块来存储100 s,而是作为一个数组的数组出来:所以每一层都是一个连续的RAM块,但整个事情不是。因此,访问相当慢:JVM必须intintarr[i][j]

  1. 查找存储在int[]arr[i];
  2. 为此编制索引以查找存储在 的 。intarr[i][j]

这涉及查询对象以从一个层转到下一个层,这相当昂贵。

为什么Java会这样做

在一个层面上,不难看出为什么不能将其优化为简单的缩放和添加查找,即使它全部分配在一个固定的块中。问题是,这是一个完全独立的引用,它可以被更改。因此,尽管数组的大小是固定的,但我们可以很容易地编写arr[3]

arr[3] = new int[11];

现在,缩放和添加被搞砸了,因为这一层已经增长。您需要在运行时知道所有内容是否仍与以前相同。此外,当然,这将被分配到RAM中的其他地方(它必须是,因为它比它所替换的更大),所以它甚至不在正确的位置进行缩放和添加。

这有什么问题

在我看来,这并不理想,原因有二。

首先,它很。对于多维情况,我使用这些方法对单维或多维数组的内容进行求和的测试花费的时间几乎是其两倍(714秒对371秒)(分别填充随机值的an和a,使用热缓存运行1000000次)。int[1000000]int[100][100][100]int

public static long sumSingle(int[] arr) {
    long total = 0;
    for (int i=0; i<arr.length; i++)
        total+=arr[i];
    return total;
}

public static long sumMulti(int[][][] arr) {
    long total = 0;
    for (int i=0; i<arr.length; i++)
        for (int j=0; j<arr[0].length; j++)
            for (int k=0; k<arr[0][0].length; k++)
                total+=arr[i][j][k];
    return total;
}   

其次,因为它很慢,因此鼓励晦涩难懂的编码。如果您遇到一些性能关键型问题,而多维数组自然会完成,那么您有动力将其编写为平面数组,即使这使得该数组不自然且难以阅读。你剩下一个令人不快的选择:晦涩难懂的代码或缓慢的代码。

对此可以做些什么

在我看来,基本问题可以很容易地得到解决。正如我们之前所看到的,它无法优化的唯一原因是结构可能会发生变化。但是Java已经有了一种使引用不可更改的机制:将它们声明为。final

现在,只需声明它

final int[][] arr = new int[10][10];

还不够好,因为它只是在这里:仍然不是,并且可以改变,所以结构可能仍然会改变。但是,如果我们有一种方法来声明事物,使其贯穿始终,除了在存储值的底层,那么我们将拥有一个完整的不可变结构,并且可以将其全部分配为一个块,并使用scale-and-add进行索引。arrfinalarr[3]finalint

它在语法上看起来如何,我不确定(我不是语言设计师)。或

final int[final][] arr = new int[10][10];

虽然不可否认,这看起来有点奇怪。这意味着:在顶层; 在下一层;不在底层(否则值本身将是不可变的)。finalfinalfinalint

最终性将使JIT编译器能够对此进行优化,以提供单维数组的性能,从而消除以这种方式编码的诱惑,只是为了解决多维数组的缓慢性。

(我听到一个谣言说C#做了这样的事情,尽管我也听到另一个谣言说CLR实现太糟糕了,不值得拥有......也许他们只是谣言...)

问题

那么,为什么Java没有真正的多维数组的实现呢?是否有坚实的技术原因?我在这里错过了什么?

更新

一个奇怪的旁注:如果您使用运行总计而不是 .为什么与 a 有这么小的差异,而与 a 会有这么大的差异?intlongintlong

基准测试代码

我用于基准测试的代码,以防有人想尝试重现这些结果:

public class Multidimensional {

    public static long sumSingle(final int[] arr) {
        long total = 0;
        for (int i=0; i<arr.length; i++)
            total+=arr[i];
        return total;
    }

    public static long sumMulti(final int[][][] arr) {
        long total = 0;
        for (int i=0; i<arr.length; i++)
            for (int j=0; j<arr[0].length; j++)
                for (int k=0; k<arr[0][0].length; k++)
                    total+=arr[i][j][k];
        return total;
    }   

    public static void main(String[] args) {
        final int iterations = 1000000;

        Random r = new Random();
        int[] arr = new int[1000000];
        for (int i=0; i<arr.length; i++)
            arr[i]=r.nextInt();
        long total = 0;
        System.out.println(sumSingle(arr));
        long time = System.nanoTime();
        for (int i=0; i<iterations; i++)
            total = sumSingle(arr);
        time = System.nanoTime()-time;
        System.out.printf("Took %d ms for single dimension\n", time/1000000, total);

        int[][][] arrMulti = new int[100][100][100];
        for (int i=0; i<arrMulti.length; i++)
            for (int j=0; j<arrMulti[i].length; j++)
                for (int k=0; k<arrMulti[i][j].length; k++)
                    arrMulti[i][j][k]=r.nextInt();
        System.out.println(sumMulti(arrMulti));
        time = System.nanoTime();
        for (int i=0; i<iterations; i++)
            total = sumMulti(arrMulti);
        time = System.nanoTime()-time;
        System.out.printf("Took %d ms for multi dimension\n", time/1000000, total);
    }

}

答案 1

但似乎这真的不是人们所期望的。

为什么?

考虑到形式意味着“T型数组”,那么就像我们期望表示“int型数组”一样,我们期望表示“int型数组的数组”,因为有的理由不少于.T[]int[]int[][]int[]Tint

因此,考虑到一个人可以拥有任何类型的数组,它只是从方式上遵循并用于声明和初始化数组(以及,和),如果没有某种特殊规则禁止数组数组,我们就可以“免费”获得这种使用。[]{},

现在还要考虑我们可以用交错数组做一些事情,否则我们无法做到:

  1. 我们可以有“锯齿状”数组,其中不同的内部数组具有不同的大小。
  2. 我们可以在外部数组中拥有空数组,以适当地映射数据,或者允许延迟构建。
  3. 我们可以故意在数组中别名,例如 与 是 同一数组。(这可以节省一些数据集的大量成本,例如,许多Unicode属性可以映射到少量内存中1,112,064个码位的完整集合,因为属性的叶数组可以重复用于具有匹配模式的范围)。lookup[1]lookup[5]
  4. 某些堆实现可以比内存中的一个大对象更好地处理许多较小的对象。

当然,在某些情况下,这些多维数组是有用的。

现在,任何功能的默认状态都是未指定和未实现的。有人需要决定指定和实现一个功能,否则它就不存在了。

因为,如上所示,数组的数组类型的多维数组将存在,除非有人决定引入一个特殊的禁止数组数组功能。由于上述原因,数组数组很有用,因此这将是一个奇怪的决定。

相反,一种多维数组,其中数组具有可以大于 1 的已定义秩,因此与一组索引而不是单个索引一起使用,这与已定义的内容自然不一致。有人需要:

  1. 确定声明,初始化和使用的规范将起作用。
  2. 记录它。
  3. 编写实际代码来执行此操作。
  4. 测试代码以执行此操作。
  5. 处理错误,边缘情况,实际上不是错误的错误报告,修复错误引起的向后兼容性问题。

用户还必须学习此新功能。

所以,它必须是值得的。一些值得的事情是:

  1. 如果没有办法做同样的事情。
  2. 如果做同样的事情的方式很奇怪或不为人所知。
  3. 人们会从类似的环境中期待它。
  4. 用户无法自行提供类似的功能。

在这种情况下:

  1. 但是有。
  2. C和C++程序员和Java在其语法上构建,因此相同的技术直接适用,因此在数组中使用步幅已经知道。
  3. Java的语法基于C++,C++同样只直接支持多维数组作为数组的数组。(除非是静态分配的,但这不是在Java中可以类比数组是对象的东西)。
  4. 人们可以很容易地编写一个类,该类包装一个数组和步幅大小的详细信息,并允许通过一组索引进行访问。

实际上,问题不在于“为什么Java没有真正的多维数组”?但是“为什么要这样做呢?

当然,你提出的支持多维数组的观点是有效的,出于这个原因,有些语言确实有它们,但负担仍然是争论一个功能,而不是争论它。

(我听到一个谣言说C#做了这样的事情,尽管我也听到另一个谣言说CLR实现太糟糕了,不值得拥有......也许他们只是谣言...)

像许多谣言一样,这里有一个真理的元素,但它不是全部的真理。

.NET 数组确实可以有多个秩。这并不是它比Java更灵活的唯一方法。每个等级也可以有一个除零以外的下限。因此,例如,您可以有一个从-3到42的数组,或者一个二维数组,其中一个等级从-2到5,另一个从57到100,或者其他什么。

C# 不从其内置语法中提供对所有这些的完全访问(您需要调用除零以外的下限),但它允许您将语法用于 的二维数组 ,用于三维数组等。Array.CreateInstance()int[,]intint[,,]

现在,处理除零以外的下限所涉及的额外工作增加了性能负担,但这些情况相对较少见。因此,下限为 0 的单秩数组被视为具有更高性能实现的特殊情况。事实上,它们在内部是一种不同的结构。

在 .NET 中,下限为零的多维数组被视为下限恰好为零的多维数组(即,作为较慢情况的示例),而不是能够处理大于 1 的秩的较快情况。

当然,.NET本可以有一个基于零的多维数组的快速路径案例,但是Java没有应用它们的所有原因,以及已经有一个特殊情况的事实,特殊情况很糟糕,然后会有两个特殊情况,它们会更糟糕。(实际上,尝试将一种类型的值分配给另一种类型的变量可能会遇到一些问题)。

上面没有一件事清楚地表明Java不可能有你所说的那种多维数组;这将是一个足够明智的决定,但做出的决定也是明智的。


答案 2

我想,这应该是詹姆斯·高斯林(James Gosling)的问题。Java的最初设计是关于OOP和简单性的,而不是关于速度的。

如果您对多维数组的工作方式有更好的了解,有几种方法可以使其栩栩如生:

  1. 提交 JDK 增强建议
  2. 通过 Java Community Process 开发新的 JSR。
  3. 提出新项目

UPD。当然,您不是第一个质疑Java数组设计问题的人。
例如,苏门答腊岛巴拿马项目也将受益于真正的多维阵列。

“Arrays 2.0”是 John Rose 在 2012 年 JVM 语言峰会上关于这个主题的演讲。