Java 是如何支持 for each 方式的循环的

67 阅读4分钟

Java 是如何支持 for each 方式的循环的

结论

  1. 如果 for each 的对象是一个数组,那么 javac 编译器会添加以下逻辑。
  • 获取数组长度
  • 用下标遍历数组
  1. 如果 for each 的对象是一个 Iterable 实例,那么 javac 编译器会添加以下逻辑。
  • 获取 iterator
  • 调用 iteratorhasNext() / next() 方法,从而遍历这个 Iterable 实例。

代码

Java Language Specification 14.14.2. The enhanced for statement 中提到了如下内容

image.png

可见 java 中的 for each 循环支持数组和 Iterable 对象两种情况。 我们分别来进行探索。

数组

我们将以下代码保存在 ArrayExample.java 里。

public class ArrayExample {
  int sum(int[] array) {
    int ans = 0;
    for (int element : array) {
      ans += element;
    }
    return ans;
  }
}

用如下的命令可以编译 ArrayExample.java

javac -g -parameters ArrayExample.java

编译后,会生成 ArrayExample.class

使用如下命令可以查看这个 class 文件的内容。

javap -v -p ArrayExample

traverse(int[]) 方法相关的部分如下

  int sum(int[]);
    descriptor: ([I)I
    flags: (0x0000)
    Code:
      stack=2, locals=7, args_size=2
         0: iconst_0
         1: istore_2
         2: aload_1
         3: astore_3
         4: aload_3
         5: arraylength
         6: istore        4
         8: iconst_0
         9: istore        5
        11: iload         5
        13: iload         4
        15: if_icmpge     35
        18: aload_3
        19: iload         5
        21: iaload
        22: istore        6
        24: iload_2
        25: iload         6
        27: iadd
        28: istore_2
        29: iinc          5, 1
        32: goto          11
        35: iload_2
        36: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 24
        line 4: 29
        line 7: 35
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           24       5     6 element   I
            0      37     0  this   LArrayExample;
            0      37     1 array   [I
            2      35     2   ans   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 11
          locals = [ class ArrayExample, class "[I", int, class "[I", int, int ]
          stack = []
        frame_type = 248 /* chop */
          offset_delta = 23
    MethodParameters:
      Name                           Flags
      array

从以上内容不容易直接看出来对应的 java 代码是什么样子。 我们换个思路,可以在 ArrayExample.java 中加入如下的代码,重新编译,然后对比 sum(int[])sum2(int[])code 数组(code 数组在 Code 属性 中)。

  int sum2(int[] array) {
    int ans = 0;
    int[] temp = array;
    int len = temp.length;
    for (int i = 0;i < len;i++) {
      int element = temp[i];
      ans += element;
    }
    return ans;
  }

根据我的对比,sum(int[])sum2(int[])code 数组是一样的(LocalVariableTable 属性有差异,因为我用了 templen 这样的局部变量),所以可以认为 sum(int[])sum2(int[]) 是等效的。

由此可见,如果 for each 循环的对象是一个数组,那么 javac 编译器会自动补上获取数组长度,遍历数组之类的逻辑。

Iterable 对象

我们将以下代码保存在 IterableExample.java 中。

import java.util.List;

public class IterableExample {
  int sum(List<Integer> list) {
    int ans = 0;
    for (int element : list) {
      ans += element;
    }
    return ans;
  }
}

以下命令可以编译 IterableExample.java

javac -g -parameters IterableExample.java

编译后,会生成 IterableExample.class 文件,使用以下命令可以查看这个 class 文件的内容。

javap -v -p IterableExample

sum(java.util.List<java.lang.Integer>) 方法相关的部分如下。

  int sum(java.util.List<java.lang.Integer>);
    descriptor: (Ljava/util/List;)I
    flags: (0x0000)
    Code:
      stack=2, locals=5, args_size=2
         0: iconst_0
         1: istore_2
         2: aload_1
         3: invokeinterface #7,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
         8: astore_3
         9: aload_3
        10: invokeinterface #13,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
        15: ifeq          40
        18: aload_3
        19: invokeinterface #19,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        24: checkcast     #23                 // class java/lang/Integer
        27: invokevirtual #25                 // Method java/lang/Integer.intValue:()I
        30: istore        4
        32: iload_2
        33: iload         4
        35: iadd
        36: istore_2
        37: goto          9
        40: iload_2
        41: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 32
        line 8: 37
        line 9: 40
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           32       5     4 element   I
            0      42     0  this   LIterableExample;
            0      42     1  list   Ljava/util/List;
            2      40     2   ans   I
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      42     1  list   Ljava/util/List<Ljava/lang/Integer;>;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 9
          locals = [ int, class java/util/Iterator ]
        frame_type = 250 /* chop */
          offset_delta = 30
    MethodParameters:
      Name                           Flags
      list
    Signature: #48                          // (Ljava/util/List<Ljava/lang/Integer;>;)I

由于 code 数组(code 数组在 Code 属性 中)的内容有些多,直接转化为对应的 java 代码不太容易。 我们可以尝试另写一个等效的方法 sum2(List<Integer>)。在编译后,如果 sum(List<Integer>)sum2(List<Integer>) 有相同的 code 数组,那就可以认为两个方法是等效的。 我把 IterableExample.java 修改成了如下的样子,sum(List<Integer>)sum2(List<Integer>) 有相同的 code 数组。

import java.util.List;
import java.util.Iterator;

public class IterableExample {
  int sum(List<Integer> list) {
    int ans = 0;
    for (int element : list) {
      ans += element;
    }
    return ans;
  }

  int sum2(List<Integer> list) {
    int ans = 0;
    Iterator iter = list.iterator();
    while (iter.hasNext()) {
      int element = ((Integer) iter.next()).intValue();
      ans += element;
    }
    return ans;
  }
}

由此可见,当 for each 的对象是 Iterable 对象时,javac 编译器会自动加上获取 iterator,以及利用 iterator 来进行遍历的逻辑。