【问题标题】:Why do arrays in Java need to have a pre-defined length when Objects don't?为什么Java中的数组需要有一个预定义的长度而对象没有?
【发布时间】:2021-04-09 06:53:00
【问题描述】:

对不起,如果这是一个非常愚蠢的问题,但是听到“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 位,而是一个指向 Object 的指针,它可以存储任意数量的 ints、doubles、Strings 或任何东西。它分配了 32 位的内存来存储指针,但对象本身可以存储任意数量的数据,不需要事先指定。

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

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

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

提前感谢任何花时间填写我的人。:)

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

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

【问题讨论】:

  • Java 数组是对象Object o = new int[] {1,2,3}; - 这是一个长度为 3 的数组。为什么?因为它可以容纳三个元素。不再。不少于。 Object o = new int[4]; 是一个由四个元素组成的数组(全部为0)。
  • 汽车和飞机都是“交通工具”。但是,我的车不会自动飞行。
  • "那么我可以在 myInteger 中存储的数据量没有限制"。真的吗?告诉我如何在其中存储整数 2^50。 (注意:您需要超过 32 位才能做到这一点)。我很确定它只能存储int
  • 我感觉 OP 将数字 32 与 32 位混淆了。但是有很多(表面上)不正确的事实和假设,很难知道 OP 的问题是在他的知识中还是在他的知识的书面表达中。
  • 啊,是的。我不应该为ints 分配32 的值,这只是混淆了事情。我使用 32 作为占位符数字,但现在每个人都认为我把它放在那里是为了表示 32 位......我会编辑它。

标签: java arrays object arraylist


【解决方案1】:

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

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

【讨论】:

  • 是的,我明白这一点。我不确定我的措辞是否错误,或者您是否读错了(这可能是我的错),但我知道我正在为指针分配内存......我的问题是为什么指向数组的指针需要知道如何当指向任何其他对象的指针不是时,数组是预先大的?
【解决方案2】:

您似乎误解了“存储”的含义。您说“我可以存储的数据量没有限制”,但是如果您运行 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 中那样在运行时添加额外的属性。您正在提前告诉它它需要的规模。

【讨论】:

  • 是的,我知道。抱歉,我认为我的问题有点令人困惑(你是第三个误解的人,我认为这是我的错,而不是你的错)。我实际上想说的是,我可以在myInteger 中存储任意数量的变量/方法,而无需手动向它们分配数据,但是如果我想将该数据存储在一个数组中,那么我必须告诉它有多少数据会有事先的。我知道覆盖变量不会占用更多空间。
  • @OOPSStudio 不,你不能。如果要向类添加额外的字段,则必须在编译时这样做,并且每个类都有固定的数据大小以及每个值的固定类型。数组是 JVM 处理“anything 的倍数”概念的机制。
  • 谢谢!这回答了我的问题。我将您的答案标记为已接受,它会告诉我我需要知道的内容。感谢您抽出宝贵的时间,我会在牢记您分享的内容的情况下进行更多研究。我很感激。
【解决方案3】:

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

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

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

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 实例中引用它。

【讨论】:

    【解决方案4】:

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

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

    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' 是这张地图。它是不是 INT 数组。它是一个到 int 数组的 map。在 java 中,[]. 都是“跟随地图,向下挖掘,然后打开”。

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

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

    那么 ArrayList 是如何工作的呢?

    地图盒。

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

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

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

    如果你然后运行list.add("foo"),那会很复杂:

    1. “foo”是一个对象(即宝藏),因此“foo”作为表达式解析为“foo”的映射。然后它拿着你的list藏宝图,跟着它,挖下去,打开盒子,你大喊‘喂!添加这个!',将您的藏宝图的副本交给“foo”宝藏。盒子对它的作用对你来说是不透明的——这就是 OO 的意义所在。
    2. 但是让我们深入研究一下arraylist的来源:它会做的是查询它的藏宝图到对象数组(这是私有的,你无法访问它,它位于一个只有djinn居住的隐藏隔间中宝箱可以打开),跟随它,向下挖掘,然后进入第一个插槽(为什么?因为硬币袋中的“大小硬币”目前为 0)。它将那里的地图带到无处,将其扔掉,将您的地图复制到“foo”宝藏中,然后将副本放在那里。然后,它将硬币袋中的硬币换成一分钱,表明它现在是 1 号。
    3. 如果您添加第 11 个元素,ArrayList djinn 会前往另一个宝藏,注意到没有空间,然后说:好吧,该死。好的。然后它变出一个全新的宝箱,可以容纳 15 张藏宝图,它复制旧宝藏中的 10 张地图,将它们移动到新宝藏箱,添加你添加的东西的地图副本作为第 11 个,然后去回到自己的胸前,撕下真正宝藏的地图,将其替换为新制作的宝藏地图(有15个插槽),然后将11ct硬币放入袋中。
    4. 旧的宝箱仍然在原处。如果没有人对此有任何地图(也没有人有),最终,海滩探险者会找到它,并摆脱它(那就是垃圾收集器)。

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

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

    【讨论】:

      【解决方案5】:

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

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

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

        主堆节点由固定不变的堆内存字节数表示。我将把这个1称为对象的原生大小

      • 数组有一个明确的length 字段。该字段未声明。它的类型为int,无法分配给它。实际上,每个数组实例的标头中都有一个 32 位字段,其中包含 length

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

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

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

        注意:

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

        2. 类特定大小与实例的本机大小相关。 (除了退化的情况......)

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

      1 - 该术语仅用于本答案的目的。我对这个词没有任何授权...


      例子:

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

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

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

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


      因此,常规对象的大小与数组长度之间的差异是:

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

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

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


      对于您修改后的问题:

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

      关键是在 JVM 级别,NOTHING 不会自动扩展。 Java 对象的原生大小不能改变。

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

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

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

      【讨论】:

      • "...如果不更新对象的所有引用,就无法移动堆节点。这不能有效地完成。"我不太确定这一点。复制收集器必须做到这一点,而且它们似乎足够高效。据我了解,G1GC 和 Shenandoah 都执行复制。也许您可以澄清一下您所说的“有效”是什么意思?
      • 我会澄清这一点。我的意思是,除非您运行 GC,否则您无法有效地执行此操作,并且仅执行 GC 以便您可以增长单个堆节点是非常低效的。因此,您无法有效地做到这一点。
      • 为什么不运行 GC 就不能这样做?如果我正在编写一个 JVM,什么会阻止我编写一个低级的“移动对象”操作,并将其暴露在类似 sun.misc.Unsafe 的东西中?我认为这根本不是不可能的,只是它不在JVM规范中。
      • 是的,你不能。基本上是不可能的。因为您无法找到并更新对已移动对象的所有 引用。除非您使所有数组间接单元格。 (其实已经尝试过了,但是效率不高,也不能扩展,很可能是并发瓶颈。)但是这个讨论离题太远了。
      • Shenandoah 重写引用,同时暂停时间与堆大小无关。这表明至少可以有效地重写引用。 Shenandoah 使用转发指针,并在下一次 GC 期间更新内容(不需要立即发生)。
      猜你喜欢
      • 2023-04-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-15
      • 2020-01-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多