为啥 Java 没有真正的多维数组?

Posted

技术标签:

【中文标题】为啥 Java 没有真正的多维数组?【英文标题】:Why doesn't Java have true multidimensional arrays?为什么 Java 没有真正的多维数组? 【发布时间】:2014-12-06 17:45:47 【问题描述】:

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

问题

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

背景

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

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

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

    找到存储在arr[i]int[]; 对此进行索引以查找存储在arr[i][j] 中的int

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

为什么 Java 会这样做

在一个层面上,不难看出为什么即使它全部分配在一个固定块中,也不能将其优化为简单的比例加法查找。问题是arr[3] 本身就是一个引用,并且可以更改。所以虽然数组的大小是固定的,但我们可以很容易地写出

arr[3] = new int[11];

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

有什么问题

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

一方面,它。对于多维情况(int[1000000] 和 @987654332 @ 分别填充随机的int 值,使用暖缓存运行 1000000 次。

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];

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

我不确定它在语法上的外观(我不是语言设计师)。也许

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

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

最终确定性将使 JIT 编译器能够对其进行优化,以提供与单维数组相同的性能,从而消除以这种方式编写代码的诱惑,以避开多维数组的缓慢性。

(我听到传言说 C# 会做这样的事情,虽然我也听到另一个传言说 CLR 实现太糟糕以至于不值得拥有......也许他们只是谣言......)

问题

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

更新

一个奇怪的旁注:如果您使用int 而不是long 作为运行总数,则时间差异会下降到只有几个百分点。为什么int 的差别那么小,long 的差别那么大?

基准代码

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

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);
    


【问题讨论】:

@vlatkozelka 如果我遇到的东西在我看来是一种已有近 20 年历史的成熟语言的缺陷,我的第一个假设是我错过了一些东西......并且有这里有一些非常聪明的人可能会告诉我我错过了什么...... 这个问题跑题了,因为它是粗俗的博文伪装成一个问题! Java 没有,因为它不是这样设计的。为什么C没有对象?为什么 Pascal 没有矩阵求逆运算符? @JarrodRoberson 你认为我在哪里发脾气并开始咆哮?整个过程中都会测量音调。帖子最后引出了一个明确的技术问题,询问是否有理由不包含此功能。 @JarrodRoberson 有些人只是简单地询问事情是如何运作的,或者他们为什么会这样运作。如果这是咆哮,那么它完全伪装,因为我在这里看不到这样的东西。 【参考方案1】:

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

为什么?

假设T[] 表示“T 类型的数组”,那么正如我们期望int[] 表示“int 类型的数组”一样,我们期望int[][] 表示“类型数组的数组” type int",因为将int[] 用作T 的理由不亚于int

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

现在还要考虑,我们可以用锯齿状数组做一些我们不能做的事情:

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

在某些情况下,这类多维数组很有用。

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

由于如上所示,除非有人决定引入特殊的禁止数组数组功能,否则多维数组的数组数组排序将存在。由于上述原因,数组数组很有用,因此做出这样的决定会很奇怪。

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

    决定声明、初始化和使用的规范。 记录下来。 编写实际代码来执行此操作。 测试代码以执行此操作。 处理错误、边缘情况、实际上不是错误的错误报告、由修复错误引起的向后兼容性问题。

用户还必须学习这个新功能。

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

    如果没有办法做同样的事情。 如果做同样事情的方式很奇怪或不为人所知。 人们会在类似的情况下期待它。 用户自己无法提供类似的功能。

在这种情况下:

    但是有。 C 和 C++ 程序员已经知道在数组中使用跨步,Java 建立在它的语法之上,因此可以直接应用相同的技术 Java 的语法基于 C++,而 C++ 同样仅直接支持将多维数组作为数组的数组。 (静态分配时除外,但这在 Java 中不能类比,其中数组是对象)。 可以轻松编写一个包含数组和步幅详细信息的类,并允许通过一组索引进行访问。

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

当然,您支持多维数组的观点是正确的,并且出于这个原因,某些语言确实有它们,但负担仍然是争论一个特性,而不是争论它。

(我听到传言说 C# 会做这样的事情,虽然我也听到另一个传言说 CLR 实现太糟糕以至于不值得拥有......也许他们只是谣言......)

像许多谣言一样,这里有一部分是真实的,但不是全部的真相。

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

C# 无法从其内置语法中完全访问所有这些内容(您需要调用 Array.CreateInstance() 以获得非零的下限),但它允许您使用语法 int[,] int 的二维数组,三维数组的int[,,] 等等。

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

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

当然,.NET 本可以为从零开始的多维数组提供快速路径案例,但是 Java 没有它们的所有原因 > 事实上,已经有一个特例,特例很烂,然后会有两个特例,它们会吸得更多。 (实际上,尝试将一种类型的值分配给另一种类型的变量时可能会遇到一些问题。

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

【讨论】:

这个论点,因为我们可以做出一个我们不需要的抽象,一个标准的不成立;首先使它成为更高抵抗力的路径,并且可能有许多不同的不兼容实现,其中许多存在缺陷,其次使它可以在概念证明之外使用。 @Dmitry 不,它确实成立,因为人们当然不需要它。如果他们这样做了,那么 Java 没有它就意味着人们没有使用 Java,而且很明显他们使用了。有人可能会争辩说,它仍然足够有用,他们应该拥有它,我在上面确实说过,这也是一个明智的设计决定,但没有必要决定将它排除在外是站不住脚的。【参考方案2】:

我想这应该是 James Gosling 的问题。 Java 的最初设计是关于 OOP 和简单性,而不是速度。

如果您对多维数组的工作方式有更好的了解,可以通过以下几种方式将其变为现实:

    提交JDK Enhancement Proposal。 通过Java Community Process 开发新的JSR。 提出一个新的Project。

UPD。当然,你不是第一个质疑 Java 数组设计问题的人。 例如,项目Sumatra 和Panama 也将受益于true 多维数组。

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

【讨论】:

@MarkoTopolnik - 数组数组更简单,因为可以忽略多维数组,除了编译器中的一些小细节。类似 C 的排序在很大程度上忽略了普通的旧数组。 @HotLicks 你简洁地表达了我在回答中需要这么多字的意思。 @MarkoTopolnik Arrays-of-arrays 是免费的,因为 Java 中有对象数组,每个数组都是一个对象,而多维数组本质上是新实体。他们需要额外的语言和虚拟机支持,同时不提供新功能。 正如 John Rose 所指出的,人们真的想要二维数组,他们想要他们的好处,例如缓存友好性。他还解释了如何在当前框架内获得这些好处。 并且支持“缓存友好性”将是添加新字节码以创建数组的简单问题。它仍然是数组的集合,因此将以“通常”(对于 Java)的方式进行处理,但各个数组将一起分配。 GC 需要进行适度的更改,字节码解释器或 JITC 不需要更改(添加新指令除外)。当然,javac 必须更改才能生成指令。 (而且 JITC 很可能在没有新指令的情况下通过识别模式来进行这种优化。)【参考方案3】:

在我看来,您似乎自己回答了这个问题:

... 将其编写为平面数组的动机,即使这会导致不自然且难以阅读。

所以把它写成一个易于阅读的平面数组。使用像

这样的琐碎助手
double get(int row, int col) 
    return data[rowLength * row + col];

和类似的设置器和可能的+=-等价物,您可以假装您正在使用二维数组。这真的没什么大不了的。你不能使用数组表示法,一切都会变得冗长而丑陋。但这似乎是Java方式。这与BigIntegerBigDecimal 完全相同。您不能使用大括号访问Map,情况非常相似。

现在的问题是所有这些功能有多重要?如果他们可以写x += BigDecimal.valueOf("123456.654321") + 10;,或spouse["Paul"] = "Mary";,或者使用没有样板的二维数组,或者其他什么?所有这些都很好,你可以走得更远,例如数组切片。 但没有真正的问题。在许多其他情况下,您必须在冗长和低效率之间做出选择。 恕我直言,花在此功能上的精力可以更好地花在其他地方。你的二维数组是一个新的最好的......

Java 实际上没有二维原始数组,...

它主要是一种语法糖,底层是对象数组。

double[][] a = new double[1][1];
Object[] b = a;

随着数组的具体化,当前的实现几乎不需要任何支持。你的实现会打开一堆蠕虫:

目前有 8 种基本类型,也就是 9 种数组类型,二维数组会是第 10 种吗? 3D 呢? 数组有一个特殊的对象头类型。二维数组可能需要另一个数组。 java.lang.reflect.Array 呢?为二维数组克隆它? 许多其他功能将被调整,例如序列化。

还有什么

??? x = new int[1], new int[2];

是吗?老式的 2D int[][]?互操作性如何?

我想,这一切都是可行的,但是 Java 中缺少一些更简单和更重要的东西。有些人一直需要二维数组,但很多人根本不记得他们什么时候使用过任何数组。

【讨论】:

【参考方案4】:

我无法重现您声称的性能优势。具体来说,测试程序:

public abstract class Benchmark 

    final String name;

    public Benchmark(String name) 
        this.name = name;
    

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() 
        try 
            int nextI = 1;
            int i;
            long duration;
            do 
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
             while (duration < 1000000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
         catch (Throwable e) 
            throw new RuntimeException(e);
        
    

    @Override
    public String toString() 
        return name + "\t" + time() + " ns";
    

    public static void main(String[] args) throws Exception 
        final int[] flat = new int[100*100*100];
        final int[][][] multi = new int[100][100][100];

        Random chaos = new Random();
        for (int i = 0; i < flat.length; i++) 
            flat[i] = chaos.nextInt();
        
        for (int i=0; i<multi.length; i++)
            for (int j=0; j<multi[0].length; j++)
                for (int k=0; k<multi[0][0].length; k++)
                    multi[i][j][k] = chaos.nextInt();

        Benchmark[] marks = 
            new Benchmark("flat") 
                @Override
                int run(int iterations) throws Throwable 
                    long total = 0;
                    for (int j = 0; j < iterations; j++)
                        for (int i = 0; i < flat.length; i++)
                            total += flat[i];
                    return (int) total;
                
            ,
            new Benchmark("multi") 
                @Override
                int run(int iterations) throws Throwable 
                    long total = 0;
                    for (int iter = 0; iter < iterations; iter++)
                        for (int i=0; i<multi.length; i++)
                            for (int j=0; j<multi[0].length; j++)
                                for (int k=0; k<multi[0][0].length; k++)
                                    total+=multi[i][j][k];
                    return (int) total;
                
            ,
            new Benchmark("multi (idiomatic)") 
                @Override
                int run(int iterations) throws Throwable 
                    long total = 0;
                    for (int iter = 0; iter < iterations; iter++)
                        for (int[][] a : multi)
                            for (int[] b : a)
                                for (int c : b)
                                    total += c;
                    return (int) total;
                
            

        ;

        for (Benchmark mark : marks) 
            System.out.println(mark);
        
    

在我的工作站上运行

java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

打印

flat              264360.217 ns
multi             270303.246 ns
multi (idiomatic) 266607.334 ns

也就是说,我们观察到您提供的一维代码和多维代码之间只有 3% 的差异。如果我们使用惯用的 Java(特别是增强的 for 循环)进行遍历,这种差异会下降到 1%(可能是因为边界检查是在循环取消引用的同一个数组对象上执行的,从而使即时编译器能够更完全地省略边界检查) .

因此,性能似乎不足以成为增加语言复杂性的理由。具体来说,为了支持真正的多维数组,Java 编程语言必须区分数组的数组和多维数组。 同样,程序员必须区分它们,并意识到它们之间的差异。 API 设计者必须考虑是使用数组数组还是多维数组。必须扩展编译器、类文件格式、类文件验证器、解释器和即时编译器。这将特别困难,因为不同维度计数的多维数组将具有不兼容的内存布局(因为必须存储它们的维度大小以启用边界检查),因此不能是彼此的子类型。因此,类 java.util.Arrays 的方法可能必须为每个维度计数重复,就像所有使用数组的多态算法一样。

总而言之,扩展 Java 以支持多维数组将为大多数程序提供微不足道的性能提升,但需要对其类型系统、编译器和运行时环境进行非平凡的扩展。因此,引入它们与 Java 编程语言的设计目标不一致,特别是 simple。

【讨论】:

啊,您已将total(运行计数器)的类型从long 更改为int。奇怪的是,这似乎是大幅减少时差的原因!为什么会是这样?!我已将我的基准测试代码添加到问题中。 事实上,这似乎值得提出一个新问题,我明天会写出来。 (顺便说一句,我不知道为什么你的答案被否决了,但不是我。事实上,看起来有人刚刚对这个问题的所有答案都投了反对票。) 已修复,我现在也使用long。这不会显着影响测量。 如果我使用你的测试工具,并将迭代次数减少到 20000,我得到的输出类似于我的工具。如果我将迭代次数设置为 100000,我会得到与您看到的类似的输出。如果我随后在 main 方法中更改基准测试的顺序,我将再次获得类似于我的线束的输出。因此,您看到的巨大差异可能是及时编译器做出错误决策的产物,在到达第二个循环之前触发了 main 方法,因此在解释器可以收集有关该循环的任何统计信息之前。跨度> @chiastic-security: “longint 慢” 在 Java 中测试简单循环时经常发生。请参阅 ***.com/questions/19844048/… --- 与 long 归纳变量的循环相比,带有 int 归纳变量的循环更积极地展开循环。【参考方案5】:

由于这个问题在很大程度上是关于性能的,所以让我提供一个适当的基于 JMH 的基准。我还更改了一些内容,使您的示例更简单,性能优势更突出。

在我的例子中,我将一维数组与二维数组进行比较,并使用非常短的内部维度。这是缓存最坏的情况。

我已经尝试过 longint 累加器,但没有发现它们之间的区别。我用int提交版本。

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(X*Y)
@Warmup(iterations = 30, time = 100, timeUnit=MILLISECONDS)
@Measurement(iterations = 5, time = 1000, timeUnit=MILLISECONDS)
@State(Scope.Thread)
@Threads(1)
@Fork(1)
public class Measure

  static final int X = 100_000, Y = 10;
  private final int[] single = new int[X*Y];
  private final int[][] multi = new int[X][Y];

  @Setup public void setup() 
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();
    for (int i=0; i<single.length; i++) single[i] = rnd.nextInt();
    for (int i=0; i<multi.length; i++)
      for (int j=0; j<multi[0].length; j++)
          multi[i][j] = rnd.nextInt();
  

  @Benchmark public long sumSingle()  return sumSingle(single); 

  @Benchmark public long sumMulti()  return sumMulti(multi); 

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

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

性能差异大于您测量的:

Benchmark                Mode  Samples  Score  Score error  Units
o.s.Measure.sumMulti     avgt        5  1,356        0,121  ns/op
o.s.Measure.sumSingle    avgt        5  0,421        0,018  ns/op

这是三倍以上的因素。 (请注意,时间是按每个数组元素报告的。)

我还注意到没有涉及预热:前 100 毫秒与其余的一样快。显然,这是一项非常简单的任务,解释器已经尽一切努力使其达到最佳状态。

更新

sumMulti 的内部循环更改为

      for (int j=0; j<arr[i].length; j++)
          total+=arr[i][j];

(注 arr[i].length)导致显着加速,正如 maaartinus 预测的那样。使用arr[0].length 使得不可能消除索引范围检查。现在结果如下:

Benchmark                Mode  Samples  Score   Error  Units
o.s.Measure.sumMulti     avgt        5  0,992 ± 0,066  ns/op
o.s.Measure.sumSingle    avgt        5  0,424 ± 0,046  ns/op

【讨论】:

通过使用arr[0].length 而不是arr[i].length,您可能会强制进行一些数组边界检查...至少需要进行一项额外检查。 我希望保持 OP 的方法完好无损 --- 但你是对的,重点是直接比较阵列性能。改变这意味着显着的加速:现在这个因素只有 2 倍。 有趣...尺寸非常不对称,您是否尝试过交换它们?我想这应该对我有很大帮助。当然,这会引出一个没完没了的问题,我们想要测量什么...... 这是故意的(正如我在介绍中解释的那样):我的目标是衡量最坏的情况,从而提供缓存逆境惩罚的上限。 可能您想要更大的阵列将其踢出 L2。拥有许多短数组的最大损失是增加了取消引用的数量、缺少预取以及可能重新安排内存布局。无论如何,将问题转为 90 度并交换 X 和 Y 才有意义 - 但正如您所说,基准测试必须展示最坏的情况。【参考方案6】:

如果您想要快速实现真正的多维数组,您可以编写这样的自定义实现。但你是对的......它不像数组符号那样清晰。虽然,一个简洁的实现可能非常友好。

public class MyArray
    private int rows = 0;
    private int cols = 0;
    String[] backingArray = null;
    public MyArray(int rows, int cols)
        this.rows = rows;
        this.cols = cols;
        backingArray  = new String[rows*cols];
    
    public String get(int row, int col)
        return backingArray[row*cols + col];
    
    ... setters and other stuff

为什么不是默认实现?

Java 的设计者可能不得不决定通常 C 数组语法的默认表示法的行为方式。他们有一个单一的数组表示法,可以实现数组数组或真正的多维数组。

我认为早期的 Java 设计者非常关心 Java 的安全性。似乎已经做出了许多决定,以使普通程序员(或糟糕的一天的优秀程序员)难以不搞砸。使用真正的多维数组,用户更容易通过在无用处分配块来浪费大块内存。

此外,从 Java 的嵌入式系统根源来看,他们可能发现它更有可能找到要分配的内存块,而不是真正的多维对象所需的大块内存。

当然,另一方面是多维数组真正有意义的地方会受到影响。而且你不得不使用一个库和看起来凌乱的代码来完成你的工作。

为什么它仍然没有包含在语言中?

即使在今天,从内存浪费/误用的角度来看,真正的多维数组也是一种风险。

【讨论】:

@MarkoTopolnik 经过近半个世纪的普遍使用,这不再算作“晦涩难懂”了。 如果封装得当,不会“不自然且难以阅读”。 @chiastic-security 当然,当代码变热时,至少在普通的 JVM 上,所有的 getter、setter 和更多琐碎的东西都会被内联。我不确定 android,但有些工具在编译时会这样做。 @MarkoTopolnik 我说“将近”半个世纪是因为我在 1972 年就想到了 C,虽然在此之前其他语言中确实会使用相同的技术,所以真的会超过半个世纪世纪,尽管我不确定何时为您处理对象大小(如 C,在 Java 中您只需要做row*cols + col,而不是(row*cols + col) * size_el),所以我不会将索赔推到“近半世纪”。 @MaksimDmitriev 你说得有道理。即使在当前的实现中也有可能进行浪费分配。

以上是关于为啥 Java 没有真正的多维数组?的主要内容,如果未能解决你的问题,请参考以下文章

为啥扩展元素不适合复制多维数组?

为啥扩展元素不适合复制多维数组?

参数传递之传递多维数组

多维动态数组,为啥不起作用?

为啥多维数组的枚举值不等于自身?

有没有办法在java中的多维数组中反转特定数组?