数组对象是否显式包含索引?

时间:2015-12-19 07:21:44

标签: java arrays

从学习Java的第一天起,各个网站和许多老师都告诉我,数组是连续的内存位置,可以存储所有相同类型的指定数量的数据。

由于数组是一个对象,对象引用存储在堆栈中,而实际对象存在于堆中,因此对象引用指向实际对象。

但是当我遇到如何在内存中创建数组的示例时,它们总是显示如下:

(其中对数组对象的引用存储在堆栈中,并且该引用指向堆中的实际对象,其中还有指向特定内存位置的显式索引)

Internal working of objects taught by various websites and teachers

但最近我遇到了Java的online notes,其中他们声称数组'内存中未指定显式索引。编译器只是通过在运行时查看提供的数组索引号来知道去哪里。

就像这样:

Enter image description here

阅读完笔记后,我也在Google上搜索了这个问题,但是这个问题的内容要么含糊不清,要么根本不存在。

我需要对此事进行更多澄清。数组对象索引是否明确显示在内存中?如果没有,那么Java如何在运行期间管理命令转到数组中的特定位置?

9 个答案:

答案 0 :(得分:14)

在Java中,数组是对象。请参阅JLS - Chapter 10. Arrays

  

在Java编程语言中,数组是对象§4.3.1),是动态创建的,可以分配给Object(§4.3.2)类型的变量。可以在数组上调用类Object的所有方法。

如果查看10.7. Array Members章节,您会看到索引是数组成员的一部分:

  

数组类型的成员是以下所有成员:

     

public final字段length,其中包含组件数   数组。长度可以是正数或零。

     
      
  • public方法clone,它会覆盖同名方法   在类Object中并且不抛出任何已检查的异常。返回类型   数组类型T[]的克隆方法是T[]

  •   
  • 所有成员都继承自班级Object; Object的唯一方法   没有继承的是它的克隆方法。

  •   

由于每种类型的大小都是已知的,因此您可以轻松确定阵列中每个组件的位置(第一个)。

访问元素的复杂性是O(1),因为它只需要计算地址偏移量。值得一提的是,并不是所有编程语言都会采用这种行为。

答案 1 :(得分:14)

  

数组对象是否显式包含索引?

简答:否。

更长的回答:通常不会,但理论上可以做到。

完整答案:

Java语言规范和Java虚拟机规范都没有任何保证内部如何实现数组。它需要的是数组元素由int索引号访问,其值从0length-1。实现如何实际获取或存储这些索引元素的值是实现的私有细节。

完全一致的JVM可以使用hash table来实现数组。在这种情况下,元素将是非连续的,分散在内存中, 需要记录元素的索引,以了解它们是什么。或者它可以向月球上的男人发送消息,他将数组值写在标签纸上并将它们存储在许多小文件柜中。我不明白为什么JVM会想要做这些事情,但它可以。

在实践中会发生什么?典型的JVM将数组元素的存储分配为平坦,连续的内存块。定位特定元素是微不足道的:将每个元素的固定内存大小乘以有用元素的索引,并将其添加到数组开头的内存地址:(index * elementSize) + startOfArray。这意味着数组存储只包含原始元素值,连续按索引排序。没有目的还要为每个元素存储索引值,因为内存中的元素地址意味着它的索引,反之亦然。但是,我并不认为您展示的图表试图说它明确存储了索引。该图表只是标记图表上的元素,以便您知道它们是什么。

使用连续存储并通过公式计算元素地址的技术简单且非常快速。它也只有非常少的内存开销,假设程序只分配它们真正需要的数组。程序依赖于并期望数组的特定性能特征,因此对阵列存储执行奇怪操作的JVM可能表现不佳并且不受欢迎。因此,实用 JVM将被限制为实现连续存储,或者执行类似的操作。

我只能想到有关该方案的几个变体,这些变体将非常有用:

  1. 堆栈分配或寄存器分配的数组:在优化期间,JVM可以通过escape analysis确定数组仅在一个方法中使用,如果数组也是一个小的固定大小,它然后,它将成为直接在堆栈上分配的理想候选对象,计算相对于堆栈指针的元素的地址。如果数组非常小(固定大小可能最多4个元素),JVM可以更进一步,并将元素直接存储在CPU寄存器中,所有元素访问都展开并放大。硬编码。

  2. 打包的布尔数组:计算机上最小的可直接寻址的内存单元通常是8位字节。这意味着如果JVM对每个布尔元素使用一个字节,那么布尔数组每8位就会浪费7个。如果布尔值在内存中打包在一起,则每个元素只使用1位。这种打包通常不会完成,因为提取单个字节位的速度较慢,需要特别考虑使用多线程才能安全。但是,在一些受内存限制的嵌入式设备中,压缩布尔数组可能非常有意义。

  3. 但是,这些变体都不需要每个元素都存储自己的索引。

    我想谈谈你提到的其他一些细节:

      

    数组存储指定数量的所有相同类型的数据

    正确。

    所有数组的元素都是相同的类型这一事实很重要,因为它意味着内存中的所有元素都是相同的大小。这就是通过简单地乘以它们的共同大小来定位元素的原因。

    如果数组元素类型是引用类型,那么这在技术上仍然是正确的。虽然在这种情况下,每个元素的值不是对象本身(可能具有不同的大小),而只是引用对象的地址。此外,在这种情况下,数组的每个元素引用的实际运行时类型的对象可以是元素类型的任何子类。如,

    Object[] a = new Object[4]; // array whose element type is Object
    // element 0 is a reference to a String (which is a subclass of Object)
    a[0] = "foo";
    
    // element 1 is a reference to a Double (which is a subclass of Object)
    a[1] = 123.45;
    
    // element 2 is the value null (no object! although null is still assignable to Object type)
    a[2] = null;
    
    // element 3 is a reference to another array (all arrays classes are subclasses of Object)
    a[3] = new int[] { 2, 3, 5, 7, 11 };
    
      

    数组是连续的内存位置

    如上所述,这并非必须如此,尽管在实践中几乎可以肯定。

    进一步说明,虽然JVM可能会从操作系统中分配一块连续的内存,但这并不意味着它最终会在物理RAM 中连续存在。操作系统可以为程序提供virtual address space,其行为就像是连续的,但是各个页面的内存分散在各个地方,包括物理RAM,磁盘上的交换文件,或者如果已知其内容为空,则根据需要重新生成。即使虚拟存储器空间的页面驻留在物理RAM中,它们也可以以任意顺序排列在物理RAM中,复杂的页表定义从虚拟地址到物理地址的映射。即使操作系统认为它正在处理"物理RAM",它仍然可以在模拟器中运行。我可以在层上层层叠叠,然后到达它们的底部all以找出真正发生的事情需要一段时间!

    编程语言规范的部分目的是将明显行为实现细节分开。在编程时,您通常可以单独编程到规范,而不必担心内部如何发生。然而,当您需要处理有限速度和内存的现实约束时,实现细节变得相关。

      

    由于数组是对象,对象引用存储在堆栈中,而实际对象存在于堆中,因此对象引用指向实际对象

    这是正确的,除了你所说的堆栈。对象引用可以可以存储在堆栈中(作为局部变量),但它们也可以存储为静态字段或实例字段,或者作为示例中所示的数组元素存储上方。

    另外,正如我前面提到的,聪明的实现有时可以直接在堆栈或CPU寄存器中分配对象作为优化,尽管这对您的程序的明显行为,仅对其性能没有任何影响。

      

    编译器只是通过在运行时查看提供的数组索引号来知道去哪里。

    在Java中,它不是执行此操作的编译器,而是虚拟机。数组是a feature of the JVM itself,因此编译器可以将使用数组的源代码转换为使用数组的字节码。然后是JVM的工作来决定如何实现数组,编译器既不知道也不关心它们是如何工作的。

答案 2 :(得分:10)

如您所说,数组只存储相同类型的对象。每种类型都有相应的大小,以字节为单位。例如,在int[]中,每个元素将占用4个字节,byte中的每个byte[]将占用1个字节,Object中的每个Object[]将占用1个字节单词(因为它确实是指向堆的指针)等等。

重要的是每个类型都有一个大小,每个数组都有一个类型。

然后,我们遇到了在运行时将索引映射到内存位置的问题。它实际上非常简单,因为您知道数组的起始位置,并且根据数组的类型,您知道每个元素的大小。

如果您的数组从某个内存位置N开始,您可以使用给定的索引I和元素大小S来计算您要查找的内存将位于内存地址N +(S * I)。 / p>

这就是Java在运行时找到索引的内存位置而不存储它们的方式。

答案 3 :(得分:7)

在您的第一张图片上auth_groupsarr[0]不是对数组元素的引用。它们只是该位置的说明性标签。

答案 4 :(得分:7)

除了严格用于人类消费的标签外,您的两个图表是相同且相同的。

也就是说,在第一个图中,标签arr[0]arr[1]等不是数组的一部分。它们仅用于说明目的,表明数组元素如何在内存中布局。

你被告知,即数组存储在内存中的连续位置(至少就虚拟地址而言;在现代硬件架构上,这些不需要映射到连续的物理地址),数组元素基于它们的位置大小和索引,是正确的。 (至少在...中,它在C / C ++中肯定是正确的。在大多数(如果不是全部)Java实现中几乎肯定是正确的。但是在允许稀疏数组或可以增长的数组的语言中它可能是不正确的或动态收缩。)

在堆栈中创建数组引用而数组数据放在堆上的事实是特定于实现的细节。将Java直接编译为机器代码的编译器可以不同地实现阵列存储,同时考虑目标硬件平台的特定特征。事实上,一个聪明的编译器可能会将整个堆栈中的小数组放在一起,并且只将堆用于较大的数组,以最大限度地减少垃圾收集的需要,这可能会影响性能。

答案 5 :(得分:5)

数组的引用并不总是在堆栈上。如果它是类的成员,它也可以存储在堆上。

数组本身可以包含原始值或对象的引用。无论如何,数组的数据总是相同的。然后编译器可以在没有显式指针的情况下处理它们的位置,只能使用值/引用大小和索引。

请参阅:
* Java语言规范,Java SE 8版 - Arrays
* Java虚拟机规范,Java SE 8版 - Reference Types and Values

答案 6 :(得分:5)

要理解的关键部分是为阵列分配的内存是连续的。因此,给定数组的初始元素的地址,即arr [0],这个连续的内存分配方案有助于运行时确定给定其索引的数组元素的地址。

假设我们声明了int [] arr = new int [5],其初始数组元素arr [0]位于地址100.要到达数组中的第三个元素,运行时需要执行的是跟随数学100 + ((3-1)*32) = 164(假设32是整数的大小)。所以运行时需要的只是该数组的初始元素的地址。它可以根据索引和数组存储的数据类型的大小派生出数组元素的所有其他地址。

只是一个偏离主题的注释:尽管数组占用了连续的内存位置,但地址仅在虚拟地址空间中是连续的,而不是在物理地址空间中。一个巨大的数组可能跨越多个可能不连续的物理页面,但数组使用的虚拟地址将是连续的。并且虚拟地址到物理地址的映射由OS页表完成。

答案 7 :(得分:4)

数组是一个有条件的内存分配,这意味着如果你知道第一个元素的地址,你可以通过步进到下一个内存地址来转到下一个索引。

引用数组不是数组地址,而是像普通对象一样到达地址(在内部完成)的方式。所以你可以说你有阵列开始的位置,你可以通过改变索引来移动内存地址。所以这就是内存中没有指定索引的原因;编译器只知道去哪里。

答案 8 :(得分:4)

“连续的内存位置”是一个实现细节,可能是错误的。例如,Objective-C可变阵列使用连续的内存位置。

对你来说,这大多无关紧要。您需要知道的是,您可以通过提供数组和索引来访问数组元素,并且您未知的某些机制使用数组和索引来生成数组元素。

显然不需要数组存储索引,因为例如世界上每个具有五个数组元素的数组都有索引0,1,2,3和4.我们知道这些是索引,无需存储它们。