当对象不需要时,为什么 Java 中的数组需要有一个预定义的长度?

时间:2021-01-03 03:43:43

标签: java arrays object arraylist

抱歉,如果这是一个非常愚蠢的问题,但听到“Java 数组实际上只是对象”,我认为它们需要具有预定义的长度是没有意义的?

我理解原始类型为什么这样做,例如 int myInt = 15; 分配 32 位内存来存储整数,这对我来说很有意义。但是如果我有以下代码:

class Integer{
    int myValue;

    public Integer(int myValue){
        this.myValue = myValue;
    }
}

后跟 Integer myInteger = new Integer(15);myInteger.myValue = 5;,那么我可以在 myInteger 中存储的数据量没有限制。它不限于 32 位,而是一个指向对象的指针,该对象可以存储任意数量的 intdoubleString 或任何东西。它分配了32位内存来存储指针,但是对象本身可以存储任意数量的数据,不需要事先指定。

那么为什么数组不能这样做呢?为什么我需要事先告诉数组分配多少内存?如果数组“实际上只是一个对象”,那么为什么我不能简单地说 String[] myStrings = new String[];myStrings[0] = "Something";

我是 Java 的新手,所以 100% 的可能性这是一个愚蠢的问题,并且有一个非常简单明了的答案,但我很好奇。

另外,再举一个例子,我可以说 ArrayList<String> myStrings = new ArrayList<String>();myStrings.add("Something"); 没有任何问题......那么是什么使 ArrayList 与数组不同呢?为什么需要告诉数组分配多少内存而 ArrayList 不需要?

预先感谢任何花时间填写我的人。:)

编辑:好的,到目前为止,评论中的每个人都误解了我的帖子,我觉得措辞错误是我的错。 我的问题不是“我如何定义数组?”,或者“更改变量的值是否会改变其内存使用?”,或者“指针是否存储它们指向的对象的数据?”,或者“是数组吗?”对象?”,也不是“ArrayLists 如何工作?” 我的问题是,为什么当我创建一个数组时,我需要告诉它它指向的对象有多大,但是当我创建任何其他对象时,它会自行缩放而不需要我提前告诉它任何内容? (以 ArrayLists 为例)

我希望这现在更有意义...我不知道为什么每个人都误解了? (我说错了吗?如果是这样,请告诉我,我会更改以方便他人)

5 个答案:

答案 0 :(得分:2)

<块引用>

我的问题是为什么指向数组的指针需要事先知道数组有多大,而指向任何其他对象的指针则不需要?

它没有。在这里,这运行得很好:

String[] x = new String[10];
x = new String[15];

整个“需要提前知道它有多大”仅指ARRAY OBJECT。如在,new int[10] 进入堆,它就像一个巨大的海滩,并凭空创建一个新的宝箱,大到足以容纳 10 个整数(作为原语,在这个例子中就像硬币一样)。然后它把它埋在沙子里,永远失去了。因此,为什么 new int[10]; 单凭它的寂寞就毫无用处。

当你写 int[] arr = new int[10]; 时,你仍然这样做,但你现在也制作了一张藏宝图。 X 标记该点。 'arr' 是这张地图。它不是整数数组。它是一个 map 到一个 int 数组。在 java 中,[]. 都是“跟随地图,向下挖掘,然后打开”。

arr[5] = 10; 的意思是:按照你的 arr 地图,向下挖,打开你在那里找到的箱子,你会看到它有 10 个小袋子的空间,每个小袋子​​都足够容纳一枚硬币。取第 6 个袋子。取出任何东西,放入一个 10 克拉的硬币。

不是地图需要知道地图通向多大的箱子。是胸部本身。对象也是如此,java中不可能做一个可以任意调整大小的宝箱。

那么 ArrayList 是如何工作的?

盒子里的地图。

ArrayList 在内部有一个 Object[] 类型的字段。该字段不包含对象数组。它不能。它保存一个映射到一个对象数组:它是一个引用

那么,当你创建一个新的数组列表时会发生什么?这是一个宝箱,大小固定,正好可以放两件东西:

  1. “对象阵列”宝箱的地图(它也将制作,有 10 张地图的空间,并将其埋在沙子中,并将地图存储到这个宝箱中。
  2. 一个硬币袋。里面的硬币代表列表实际包含多少个对象。通往它所拥有的宝藏的地图通向一个可容纳 10 张地图的宝藏,但是这枚硬币(价值:0)表明,到目前为止,这些地图都没有去任何地方。

如果你然后运行 ​​list.add("foo"),它的作用是复杂的:

  1. "foo" 是一个对象(即宝藏),所以作为表达式的 "foo" 解析为 a map 到 "foo"。然后它会拿走你的 list 藏宝图,跟随它,挖掘它,打开盒子,然后你大喊“哦!添加这个!',将您藏宝图的副本交给“foo”宝藏。然后这个盒子用它做什么对你来说是不透明的 - 这就是面向对象的重点。
  2. 但是让我们深入研究 arraylist 的来源:它会做的是查询它的藏宝图到对象数组(这是私有的,你无法访问它,它在一个隐藏的隔间里,只有住在里面的 djinn宝箱可以打开),跟随它,向下挖掘,然后进入第一个插槽(为什么?因为硬币袋中的“大小硬币”当前为 0)。它把那里的地图拿走,扔掉,把你的地图复制到“foo”宝藏,然后把副本放在那里。然后它用一便士替换硬币袋中的硬币,以表明它现在是 1 号。
  3. 如果添加第 11 个元素,则 ArrayList djinn 会前往另一个宝藏,发现没有空间,然后说:嗯,该死。好的。然后它召唤出一个全新的宝箱,可以容纳 15 张藏宝图,它复制旧宝藏中的 10 张地图,将它们移动到新的宝箱,添加你添加的东西的地图副本作为第 11 个,然后去回到自己的宝箱,撕下真正宝藏的地图,将其替换为新制作的宝藏的地图(有15个插槽),并在小袋中放入一个11ct的硬币。
  4. 旧的百宝箱原地不动。如果没有人有任何地图(也没有人有),最终,海滩漫步者会找到它,将其清除(那就是垃圾收集器)。

因此,ALL 宝箱的大小是固定的,但是通过用新地图替换地图并召唤新宝箱,您仍然可以使它看起来像 ArrayList 能够缩小和成长。

那为什么数组不允许呢?因为缩小和增长的东西很复杂,数组暴露了低级功能。不要使用数组,使用列表。

答案 1 :(得分:1)

您似乎误解了“存储”的含义。你说“我可以存储的数据量没有限制”,但如果你运行 myInteger.myValue = 15,你覆盖你原来放在那里的 32 的值。您仍然不能存储超过 32 位的数据,只是您可以更改您在该变量中放入的 32 位

如果你想看看ArrayList是如何工作的,你可以read the source code;它可以展开because if it runs out of space it creates a new larger array and switches its single array variable elementData to it

根据您的更新,您似乎想知道是否能够将许多不同的字段添加到对象定义中。在这种情况下,这些字段和它们的类型在类被编译时是固定的,并且从那时起类具有固定的大小。您不能像在 JavaScript 中那样在运行时堆积额外的属性。您预先告诉它它需要的规模。

答案 2 :(得分:1)

我将忽略您提供的大部分细节,并在编辑中回答问题。

<块引用>

我的问题是,为什么当我创建一个数组时,我需要告诉它它指向的对象有多大,但是当我创建任何其他对象时,它会自行缩放而不需要我事先告诉它任何事情?

从处理“当我制作任何其他对象时它会自行缩放”开始是值得的,因为这不是真的。如果你创建一个这样的类:

class MyInteger
  public int value;
  public MyInteger(int value) {
    this.value = value;
  }
}

然后该类具有静态定义的大小。一旦编译了这个类,MyInteger 实例的内存量就已经确定了。在这种情况下,它是对象头大小(取决于 JVM)和一个整数的大小(至少 4 个字节)。

对象一旦被 JVM 分配,它的大小就不能改变。 JVM(重要的是垃圾收集器)将其视为一个字节块,直到它被回收。像 ArrayList 这样的类给人一种增长的错觉,但它们实际上是通过分配其他对象来工作的,它们存储对这些对象的引用。

class MyArrayList {
  public int[] values;
  public MyArrayList(int[] values) {
    this.values = values;
  }
}

在这种情况下,MyArrayList 实例将始终占用相同数量的内存(对象头大小 + 引用大小),但引用的数组可能会发生变化。我们可以这样做:

MyArrayList list = new MyArrayList(new int[50]);

这会为 list 分配一块内存,为 list.values 分配一块内存。如果我们那么做(就像 ArrayList 在内部有效地做的那样):

list.values = new int[500];

那么分配给 list 的内存大小仍然相同,但我们分配了一个新块,然后在 list.values 中引用该块。这使得我们的旧 int[50] 没有任何引用(因此它可以被垃圾收集)。但重要的是,没有分配改变大小。我们重新分配了一个新的更大的块供我们的列表使用,并从我们的 MyArrayList 实例中引用了它。

答案 3 :(得分:1)

<块引用>

为什么 Java 中的数组需要有预定义的长度,而对象不需要?

为了理解这一点,我们需要确定“大小”在 Java 中是一个复杂的概念。有多种含义:

  • 每个对象作为一个或多个堆节点存储在堆中,其中一个是主节点,其余是可以从主节点访问的组件对象。

    主堆节点由固定且不变的堆内存字节数表示。我将把它称为1对象的本机大小

  • 一个数组有一个明确的 length 字段。该字段未声明。它的类型为 int 且无法分配给它。每个数组实例的头部实际上都有一个 32 位的字段,用于保存 length

    数组的 length 直接映射到它的本机大小。 JVM 可以根据 length 计算本机大小。

  • 不是数组实例的对象也具有本机大小。这是由对象字段的数量和类型决定的。由于无法在运行时添加或删除字段,因此本机大小不会更改。但它不需要存储,因为它可以在运行时从对象的类中确定(在需要时)。

  • 某些对象支持类特定大小概念。例如,String 的大小由其 length() 方法返回,而 ArrayList 的大小由其 size() 方法返回。

    注意:

    1. 特定于类的大小的含义是......特定于类。

    2. 特定于类的大小与实例的本机大小无关。 (退化情况除外……)

事实上,所有对象都有一个固定的原生大小。

1 - 该术语仅用于本答案。我声称对这个词没有权威......


示例:

  1. String[] 具有取决于其长度的原生大小。在典型的 JVM 上,它将是 12 + length * () 舍入为 16 字节的倍数。

  2. 您的 Integer 类具有固定的本机大小。在典型的 JVM 上,每个实例的长度为 16 字节。

  3. 一个 ArrayList 对象有 2 个 private int 字段和一个 private Object[] 字段。这为其提供了 16 或 24 字节的固定本机大小。 int 字段之一是 call size,它包含 size() 返回的值。

    sizeArrayList 可能会改变,但这是由类的代码实现的。为此,它可能需要重新分配其内部 Object[] 以使其足够大以容纳更多元素。如果您检查 ArrayList 类的源代码,您会看到这是如何发生的。 (查找 ensureCapacitygrow 方法。)


所以,普通对象的大小和数组的长度之间的区别是:

  • 普通对象的自然大小完全由对象的类型决定,它永远不会改变。它很少与应用程序相关,也不会通过字段公开。

  • 数组的长度取决于实例化它时提供的值。它永远不会改变。自然大小可以从长度上确定。

  • 对象的类特定大小(如果相关)由类管理。


对于您修改后的问题:

<块引用>

我的问题是,为什么当我创建一个数组时,我需要告诉它它指向的对象有多大,但是当我创建任何其他对象时,它会自行缩放而无需我事先告诉它任何事情? (以 ArrayLists 为例)

关键是在 JVM 级别,没有任何东西可以自动扩展。 Java 对象的本机大小不能改变。

为什么?因为增加对象的堆节点的大小将需要移动堆节点,并且在不更新对象的所有引用的情况下无法移动堆节点。这无法有效地完成。

(有人指出 GC 可以有效地移动堆节点。然而,这不是一个可行的解决方案。运行 GC 是昂贵的。执行 GC 以(例如)增长一个单个 Java 数组。如果已指定 Java 以便数组可以“增长”,则需要使用底层的不可增长数组类型来实现。)

ArrayList 的情况由 ArrayList 类本身处理,它通过(如有必要)创建一个新的更大的后备数组,将元素从旧元素复制到新元素,然后丢弃旧的支持数组。它还调整保存列表逻辑大小的 size 字段。

答案 4 :(得分:0)

对象数组为对象指针分配空间,而不是内存中的整个对象。

所以 new String[10] 不会为 10 个字符串分配空间,而是为 10 个对象引用分配空间,这些对象引用将指向数组中存储的字符串。

相关问题