Java中array.length()的代码内部是什么?

4
数组的第10个位置存储了什么?
int[] array=new int[10];

假设我们有从 array[0]array[9] 存储的值,如果我想要打印元素而不使用

array.length()

或者 for (int a: array)

我该怎么做?

我的基本问题是JVM如何确定数组的结尾,是在解析数组时遇到null还是遇到垃圾值时结束?array.length()函数的内置代码是什么?


6
length()不是一个方法,而是一个final字段 - 因此您不需要在其后面加上() - Michael Berry
你仍然可以在Java中使用“哨兵值”:在C中,读取数组/“对象”之外是“未定义行为”,而在Java中则是保证的异常。但是,在表现良好的情况下,它们是相同的。但是,由于Java数组“知道”它的大小(不像C中的指向数组的指针),因此即使可以使用哨兵,也不需要哨兵。想象一下在C中的情况:struct myarray_t { int length; void* data; } - user166390
9个回答

5

数组的第10个位置存储了什么?

Java与C/C++在数组方面使用不同的范例。C/C++使用终止符(也称为“垃圾”)值,如NULL来表示数组的结尾。在Java中,数组更像是具有特殊“实例变量”(instance variable)的对象,该变量length指示数组中有多少个插槽。这个特殊的“实例变量”在数组创建时设置,是只读的。可以通过说array.length来访问它。

Java希望代码知道何时停止到达数组的结尾,方法是确保它们不指定大于length - 1的索引。但是,JVM出于安全原因检查每次访问数组。如果JVM发现一个小于0或大于length - 1的数组索引,则JVM会抛出IndexOutOfBoundsException异常。

如果我要打印元素而不使用array.length()

由于我们总是可以检查长度,因此在Java中不需要在数组结尾处使用标记。在最后一项之后没有任何特殊的内容(它可能是其他变量的内存)。

for(int a: array) {
    // code of loop body here
}

这段代码会被编译器神奇地转换为:

for (int i = 0; i < array.length; i++) {
    int a = array[i];
    // code of loop body here
}

然而,i索引变量对用户的代码不可访问。这段代码仍然隐式地使用了array.length。

1
+1 展示了神奇的转换 - 在JLS或类似的文献中有参考吗? - user166390
它被称为“增强型for循环”。请参见此处的II.语义http://jcp.org/aboutJava/communityprocess/jsr/tiger/enhanced-for.html,以及http://java.sun.com/docs/books/jls/third_edition/html/statements.html#14.14.2(“然后给出增强型for语句的含义...”)。 - Bert F
1
事实上,.length 变量可以被称为常量 - Paŭlo Ebermann

4

数组是带有长度字段的对象。在循环时,Java会加载长度字段并将迭代器与其进行比较。

请参见JLS中的10.7 数组成员


你好Roflcoptr,在上面的例子中,如果不使用array.length()或我提到的for循环,你将如何打印元素。我感到困惑,你能详细说明一下吗?谢谢。 - ranjanarr
不是开发人员明确使用长度字段,而是编译器。 - RoflcoptrException
在这种情况下,您能展示一下如何打印元素而不使用我们上面讨论过的方法吗?这个问题背后的动机是:C语言存储了一个空值,开发人员可以在for循环中提到它以停止超出该点并打印null之前的所有元素,那么您会如何在Java中实现这一点呢? - ranjanarr
我不确定你为什么想这样做,但如果你非常想要这样做,你应该检查一下是否出现了“java.lang.ArrayIndexOutOfBoundsException”。 - RoflcoptrException
有没有一个定义 for(:) 的参考链接?例如,它在处理 arrayCollection 时如何工作?我想在前者中会有特殊情况。 - user166390

2

JVM内部可以以任何它认为合适的方式跟踪数组的长度。实际上,当您尝试获取数组的长度时,Java编译器会发出一个名为arraylength的字节码指令,这表明由JVM决定跟踪数组长度的最佳方法。

大多数实现可能将数组存储为一块内存,其第一个条目是数组的长度,其余元素是实际的数组值。这使得实现可以在O(1)中查询数组的长度以及数组中的任何值。但是,如果实现希望的话,它可以将元素存储在哨兵值后面(如您所建议的)。但我不相信任何实现都会这样做,因为查找长度的成本将随数组大小呈线性增长。

至于foreach循环的工作原理,编译器将该代码转换为类似于以下内容的东西:

for (int i = 0; i < arr.length; ++i) {
    T arrayElem = arr[i];
    /* ... do work here ... */
}

最后,关于一个10元素数组的第十个元素是什么,没有保证这个位置上有对象。JVM可以很容易地分配数组空间,使得没有第十个元素。由于在Java中您无法实际获取此值(如果尝试获取将抛出异常),因此JVM甚至没有义务在那里放置有意义的内容。
希望这能帮到您!

这个问题的动机是:C语言存储了一个空值,开发人员可以在for循环中提到它,以停止超出该点并打印空值之前的所有元素。在Java中,您将如何实现此操作?不使用array.length,我同意array.length是传统的方法,但我正在寻找其他可用的选项。谢谢。 - ranjanarr
@ranjanarr- 你关于C数组的说法只是部分正确的。C风格的字符串确实在字符串末尾存储了一个NUL字节来标记结尾,但更一般的C风格数组并不这样做。事实上,如果您尝试为任何其他类型的C数组执行此操作,您可能会通过读取超出数组末尾而使程序崩溃。此外,这种设计模式极难正确使用;只需看看getsstrcpy等中的所有安全漏洞即可。我认为在Java中采用这种方式并不是一个好主意,因为您已经知道数组的大小。 - templatetypedef
1
@ranjanarr:这不是C语言。你为什么要把C语言的语法强加到Java上呢?在C语言中这样做的原因是因为C语言中没有对象,而以null作为数组结尾是唯一的方法。 - Falmarri
@templatetypedef 我想象中arrays的渐进性能要求在JLS的某个地方有记录。 - user166390
@Falmarri 实际上C语言中存在“对象”(考虑:可以访问的数据)。 C中的缓冲区溢出发生在未访问所需对象时;-)此外,关于在C中需要哨兵值与单独传递长度或使用离散结构表示此操作之间没有固有的要求。只是一些学究式的评论。 - user166390
显示剩余2条评论

1

好的,开始吧 :-)

C语言处理"数组"的方法

在C语言中,有很多种处理数组的方式。接下来,我将讲述关于string*的内容(并使用类型为string*的变量strings)。这是因为t[]"有效地分解"成t*,而char*是"C字符串"的类型。因此,string*表示指向"C字符串"的指针。这掩盖了关于C语言中"数组"和"指针"的一些追求严谨的问题。(请记住:只因为指针可以被访问为p[i]并不意味着在C语言中它是一个数组类型。)

现在,strings(类型为string*)没有办法知道它的大小——它只代表某个字符串的指针或者可能是NULL。现在,让我们看看一些我们可以"知道"大小的方法:

使用哨兵值。 在这里,我假设使用NULL作为哨兵值(或者对于整数“数组”,它可能是-1等)。请记住,C语言没有要求数组必须有哨兵值,因此这种方法与下面的两种方法一样,只是惯例

string* p;
for (p = strings; p != NULL; p++) {
   doStuff(*p);
}

外部跟踪数组大小。

void display(int count, string* strings) {
  for (int i = 0; i < count; i++) {
    doStuff(strings[i]);
  }
}

将 "array" 和长度捆绑在一起。
struct mystrarray_t {
  int size;
  string* strings;
}

void display(struct mystrarray_t arr) {
  for (int i = 0; i < arr.size i++) {
    doStuff(arr.strings[i]);
  }
}

Java使用这种最后的方法。

在Java中,每个数组对象都有一个固定的大小,可以通过arr.length访问。有特殊的字节码魔法使其工作(在Java中,数组非常神奇),但在语言级别上,这只是一个只读整数字段,永远不会改变(请记住,每个数组对象都有一个固定的大小)。编译器和JVM/JIT可以利用这个事实来优化循环。

与C不同,Java 保证尝试访问超出边界的索引导致异常(出于性能原因,即使它没有被公开,这也需要JVM跟踪每个数组的长度)。在C中,这只是未定义的行为。例如,如果哨兵值不在对象内部(读作“所需访问的内存”)中,则示例#1将导致缓冲区溢出。

然而,在Java中使用哨兵值是没有任何问题的。与带有哨兵值的C形式不同,这也可以避免IndexOutOfBoundExceptions(IOOB),因为长度保护是最终限制。哨兵只是一个提前退出。

// So we can add up to 2 extra names later
String names[] = { "Fred", "Barney", null, null };
// This uses a sentinel *and* is free of an over-run or IOB Exception
for (String n : names) {
  if (n == null) {
    break;
  }
  doStuff(n);
}

或者可能允许IOOB异常,因为我们做了一些愚蠢的事情,比如忽略了数组知道它们的长度这个事实:(有关“性能”的评论请参见)。

// -- THERE IS NO EFFECTIVE PERFORMANCE GAIN --
// Can ONLY add 1 more name since sentinel now required to
// cleanly detect termination condition.
// Unlike C the behavior is still well-defined, just ill-behaving.
String names[] = { "Fred", "Barney", null, null };
for (int i = 0;; i++) {
  String n = strings[i];
  if (n == null) {
    break;
  }
  doStuff(n);
}

另一方面,我不鼓励使用这种原始的代码——最好只在几乎所有情况下使用适当的数据类型,例如List。

编码愉快。


1
--> -- 没有有效的性能提升 -- 存在性能下降,VM 仅在循环结束后一次检查边界;for(int i=0;i<a.length;i++) 只被检查一次。关于蔑视部分,数组具有非常好的优化,并允许实现原子访问(不像集合)。 - bestsss
@bestsss 感谢你提供的边界检查信息。关于后者,你绝对是正确的 - 虽然在我的工作中,我还没有遇到过它真正起作用的情况。(我猜测使用Java编写3D游戏引擎的人可能有不同的经历。) - user166390
有趣的是,3D实际上需要DirectBuffers来与本地部分和GPU通信。 - bestsss

1

定义“垃圾值”的含义。(提示:由于一切都是二进制的,除非使用哨兵值,否则根本不存在这样的东西,而这只是不好的做法)。

数组的长度存储在Array实例中作为成员变量。这并不复杂。


你好 Donnie,我的例子中第11个单元格,即array[10]位置存储了什么?我们知道如果打印超出边界会导致JVM错误。但我很困惑。谢谢。 - ranjanarr
在C/C++中,这可能不是一个异常:但它仍然是未定义的行为(程序可以自由崩溃或吃掉你冰箱里所有的菠菜)。Java只是使行为明确定义。我在主帖中添加了一条评论。 - user166390
这稍微有点复杂,因为数组在字节码和类型方面都有特殊的魔法,但是...没错。大概就是这样+1。 - user166390

1
在另一个评论中,原帖作者写道:
“我同意array.length是常规方法,但如果有其他选项的话,我想找找看。”
JVM实现者没有其他合理的实现选项……在任何主流硬件架构上都一样。
特别地,哨兵方法只能检测应用程序在索引结束后获取数组元素一个的情况。
  • 如果它获取2个或更多索引,则会错过哨兵并继续访问内存,其内容未知。
  • 如果它存储,则不会查询哨兵。
  • 如果它需要直接访问数组大小作为应用程序算法的一部分,则寻找哨兵是非常低效的方法。(更不用说不可靠了;例如,如果null是有效的数组元素。)
  • 哨兵对于(大多数)原始数组无效,因为没有可以用作哨兵的值。(从JLS的角度来看,原始数组持有null是荒谬的概念,因为null与任何Java原始类型都不兼容。)
  • 垃圾回收器在所有情况下都需要数组长度。
简而言之,必须在数组中存储长度以处理其他情况。同时存储哨兵意味着浪费空间存储冗余信息和 CPU 周期创建哨兵并将其复制(在 GC 中)。

0
你如何在不使用array.length或foreach循环的情况下打印元素?
当然,你可以在不进行边界检查的情况下遍历数组,然后在最后捕获(并忽略)ArrayIndexOutOfBoundsException异常:
try {
  int i = 0;
  while (true) {
    System.out.println(arr[i++]);
  }
catch (ArrayIndexOutOfBoundsException e) {
  // so we are past the last array element...
}

这在技术上是可行的,但这是不好的实践。你不应该使用异常来控制流程。


0
在不使用 for each 循环或者 length 字段的情况下,如何打印数组中的所有元素呢?实话实说,你根本不需要这么做。你可以使用下面这样的 for 循环:
try {
    for(int i=0 ; ; i++) {
        System.out.println(arr[i]);
    }
}
catch(IndexOutOfBoundsException ex) {}

但那是一种可怕的做事方式!


这个问题的动机是:C语言存储了一个空值,开发人员可以在for循环中提到它,以停止超出该点并打印空值之前的所有元素,我想知道是否有一个空引用来在遇到它时停止打印元素。 - ranjanarr
1
这在C语言中是存在的,让您知道何时到达数组的末尾,因为如果告诉它会导致混乱,C语言将很高兴地让您读取/写入超出数组边界的内容! 在Java中不是这样,如果尝试,则会导致IndexOutOfBoundsException异常,这更符合Java的行为方式。 不管怎样,由于多种原因,异常不应用作正常流程控制的一种方法,因此在实践中绝对最好避免使用此方法。 - Michael Berry
请注意,在C语言中,许多数组没有哨兵。这是C语言的惯例。 - user166390

0

所有在区间 [0,9] 之外的数组访问都会导致 ArrayIndexOutOfBoundsException,而不仅仅是位置 10。因此,从概念上说,你可以说你的整个内存(通过索引从Integer.MIN_VALUEInteger.MAX_VALUE)都填充了哨兵值,除了数组本身的空间外,当读取或写入一个被哨兵填充的位置时,就会触发异常。(每个数组都有自己的整个内存要使用)。

当然,实际上没有人拥有每个数组都要使用的整个内存,因此 VM 实现了一些更智能的数组访问方式。你可以想象成像这样:

class Array<X> {

   private final int length;
   private final Class<X> componentType;

   /**
    * invoked on   new X[len] .
    */
   public Array<X>(int len, Class<X> type) {
      if(len < 0) {
          throw new NegativeArraySizeException("too small: " + len);
      }
      this.componentType = type;
      this.len = len;
      // TODO: allocate the memory

      // initialize elements:
      for (int i = 0; i < len; i++) {
          setElement(i, null);
      }
   }


   /**
    *  invoked on   a.length
    */
   public int length() {
       return length;
   }


   /**
    * invoked on   a[i]
    */
   public X getElement(int index) {
      if(index < 0 || length <= index)
         throw new ArrayIndexOutOfBoundsException("out of bounds: " + index);
      // TODO: do the real memory access
      return ...;
   }

   /**
    * invoked on   a[i] = x
    */
   public X setElement(int index, X value) {
      if(index < 0 || length <= index) {
         throw new ArrayIndexOutOfBoundsException("out of bounds: " + index);
      }
      if(!componentType.isInstance(value)) {
         throw new ArrayStoreException("value " + value + " is of type " +
                                       value.getClass().getName() + ", but should be of type "
                                       + componentType.getName() + "!");
      }
      // TODO: do the real memory access
      return value;
   }

}

当然,对于原始值,组件类型检查会更简单一些,因为编译器(然后是VM字节码验证器)已经检查了是否有正确的类型,有时还进行类型转换。 (初始化将使用类型的默认值,而不是null。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接