文章目录
  1. 1. 〇、前言
  2. 2. 一、集合遍历测试数据对比
    1. 2.0.1. 1.1 LinkedList循环测试数据对比
    2. 2.0.2. 1.2 ArrayList循环测试数据对比
  • 3. 二、数组遍历测试数据对比
  • 4. 三、从字节码看foreach循环
    1. 4.0.1. 3.1 LinkedList测试代码的字节码
    2. 4.0.2. 3.2 ArrayList测试代码的字节码
    3. 4.0.3. 3.3 数组测试代码的字节码
  • 5. 四、总结
  • 〇、前言

    前几天看到群里有人发了一篇关于java foreach循环(增强for循环)测试的文章。文章没细看,从别人的发的截图来看是将LinkedList使用for i循环与foreach循环测试的数据做对比,然后得出foreach循环惊人的性能。猜写这文章的人应该是一个初学者,如果是我,会用迭代器来遍历LinkedList,然后再与foreach循环的数据作对比。因为LinkedList是双链表结构,并不适合使用for i循环这种方式进行遍历,实际写代码时也不会这么用。那foreach循环出现是为了解决什么问题?

    一、集合遍历测试数据对比

    使用的测试环境是jdk: 1.8.0_251,CPU:i7-9750H。

    1.1 LinkedList循环测试数据对比

    对于LinkedList遍历的测试代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    import java.util.Iterator;
    import java.util.LinkedList;

    public class LinkedListTest {

    private static final int SIZE = 50000000;
    private static final int TIMES = 20;

    public static void main(String[] args) {
    LinkedList<String> list = initLinkedList();
    long time = System.currentTimeMillis();
    for (int i = 0; i < TIMES; i++) {
    testLinkedListIterator(list);
    // testLinkedListForeach(list);
    }

    System.out.println("LinkedList used time: " + ((System.currentTimeMillis() - time) / TIMES));
    }

    /**
    * 初始化数据
    * @return 返回初始化数据
    */
    private static LinkedList<String> initLinkedList() {
    LinkedList<String> list = new LinkedList<>();
    for (int i = 0; i < SIZE; i++) {
    list.add(String.valueOf(i));
    }

    return list;
    }

    /**
    * 测试迭代器迭代LinkedList的性能
    * @param list LinkedList数据
    * @return 返回执行的时间
    */
    private static long testLinkedListIterator(LinkedList<String> list) {
    long sum = 0L;
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
    sum += it.next().length();
    }

    return sum;
    }

    /**
    * 测试foreach循环LinkedList的性能
    * @param list LinkedList数据
    * @return 返回执行的时间
    */
    private static long testLinkedListForeach(LinkedList<String> list) {
    long sum = 0L;
    for (String s : list) {
    sum += s.length();
    }

    return sum;
    }

    }

    最后测试两种遍历方式输出的时间分别是:

    1
    2
    testLinkedListIterator(): LinkedList used time: 1005
    testLinkedListForeach(): LinkedList used time: 1028

    通过数据对比,迭代器循环和foreach循环两者不相上下。

    1.2 ArrayList循环测试数据对比

    同样测试ArrayList遍历的代码如下,但是增加了普通的for循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    import java.util.ArrayList;
    import java.util.Iterator;

    public class ArrayListTest {

    private static final int SIZE = 50000000;
    private static final int TIMES = 20;

    public static void main(String[] args) {
    ArrayList<String> list = initArrayList();
    long time = System.currentTimeMillis();
    for (int i = 0; i < TIMES; i++) {
    testArrayListIterator(list);
    // testArrayListForeach(list);
    // testArrayListFor(list);
    }

    System.out.println("ArrayList used time: " + ((System.currentTimeMillis() - time) / TIMES));
    }

    /**
    * 初始化数据
    * @return 返回初始化数据
    */
    private static ArrayList<String> initArrayList() {
    ArrayList<String> list = new ArrayList<>(SIZE);
    for (int i = 0; i < SIZE; i++) {
    list.add(String.valueOf(i));
    }

    return list;
    }

    /**
    * 测试迭代器迭代ArrayList的性能
    * @param list ArrayList数据
    * @return 返回执行的时间
    */
    private static long testArrayListIterator(ArrayList<String> list) {
    Iterator<String> it = list.iterator();
    long sum = 0L;
    while (it.hasNext()) {
    sum += it.next().length();
    }

    return sum;
    }

    /**
    * 测试foreach循环ArrayList的性能
    * @param list ArrayList数据
    * @return 返回执行的时间
    */
    private static long testArrayListForeach(ArrayList<String> list) {
    long sum = 0L;
    StringBuilder sb = new StringBuilder();
    for (String s : list) {
    sum += s.length();
    }

    return sum;
    }

    /**
    * 测试普通for循环ArrayList的性能
    * @param list ArrayList数据
    * @return 返回执行的时间
    */
    private static long testArrayListFor(ArrayList<String> list) {
    long sum = 0L;
    for (int i = 0; i < list.size(); i++) {
    sum += list.get(i).length();
    }

    return sum;
    }

    }

    最后测试三种遍历方式输出的时间分别是:

    1
    2
    3
    testArrayListIterator(): ArrayList used time: 162
    testArrayListForeach(): ArrayList used time: 164
    testArrayListFor(): ArrayList used time: 166

    同样,三者的数据基本不相上下。

    二、数组遍历测试数据对比

    对于普通的数组也可以使用foreach循环,测试代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59

    public class ArrayTest {

    private static final int SIZE = 50000000;
    private static final int TIMES = 20;

    public static void main(String[] args) {
    String[] list = initArray();
    long time = System.currentTimeMillis();
    for (int i = 0; i < TIMES; i++) {
    testArrayForeach(list);
    // testArrayFor(list);
    }

    System.out.println("Array used time: " + ((System.currentTimeMillis() - time) / TIMES));
    }

    /**
    * 初始化数据
    * @return 返回初始化数据
    */
    private static String[] initArray() {
    String[] list = new String[SIZE];
    for (int i = 0; i < SIZE; i++) {
    list[i] = String.valueOf(i);
    }

    return list;
    }

    /**
    * 测试foreach循环数组的性能
    * @param list 数组数据
    * @return 返回执行的时间
    */
    private static long testArrayForeach(String[] list) {
    long sum = 0L;
    for (String s : list) {
    sum += s.length();
    }

    return sum;
    }

    /**
    * 测试普通for循环数组的性能
    * @param list 数组数据
    * @return 返回执行的时间
    */
    private static long testArrayFor(String[] list) {
    long sum = 0L;
    for (int i = 0; i < list.length; i++) {
    sum += list[i].length();
    }

    return sum;
    }

    }

    测试两种遍历方式输出的时间分别是:

    1
    2
    testArrayForeach(): Array used time: 152
    testArrayFor(): Array used time: 150

    一样区分不了伯仲。

    三、从字节码看foreach循环

    从上面的测试的数据来看,集合的foreach循环和迭代器循环耗时基本一致,没有很大差异。同样,数组的foreach循环和普通循环也没有很大的差异。接下来看下编译后的代码来进行对比,因最终都是编译成字节码交给虚拟机运行,所以看编译后的结果最直接,看看foreach循环的代码有什么不同。

    下面就以smali格式来查看字节码,因为smali这种方式跟java语法类似,感觉比不停的出栈、入栈要容易理解。

    查看的方法是下载jadx工具,直接将class文件拖入到打开的窗口中,在右边的窗口中的左下角有一个代码smali切换的Tab,点击smali就可以看到smali格式的代码。

    3.1 LinkedList测试代码的字节码

    下面只贴出核心部分的代码,想看完整的代码,可以自己动手试下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    .method private static testLinkedListForeach(Ljava/util/LinkedList;)J
    // 头部省略
    .prologue
    .line 56
    .local p0, "list":Ljava/util/LinkedList;, "Ljava/util/LinkedList<Ljava/lang/String;>;"
    const-wide/16 v2, 0x0

    .line 57
    .local v2, "sum":J
    invoke-virtual {p0}, Ljava/util/LinkedList;->iterator()Ljava/util/Iterator;

    move-result-object v1

    :goto_6
    invoke-interface {v1}, Ljava/util/Iterator;->hasNext()Z

    move-result v4

    if-eqz v4, :cond_19

    invoke-interface {v1}, Ljava/util/Iterator;->next()Ljava/lang/Object;

    move-result-object v0

    check-cast v0, Ljava/lang/String;

    .line 58
    .local v0, "s":Ljava/lang/String;
    invoke-virtual {v0}, Ljava/lang/String;->length()I

    move-result v4

    int-to-long v4, v4

    add-long/2addr v2, v4

    .line 59
    goto :goto_6

    .line 61
    .end local v0 # "s":Ljava/lang/String;
    :cond_19
    return-wide v2
    .end method

    .method private static testLinkedListIterator(Ljava/util/LinkedList;)J
    // 头部省略
    .prologue
    .line 41 // 对应java代码的行号
    .local p0, "list":Ljava/util/LinkedList;, "Ljava/util/LinkedList<Ljava/lang/String;>;" // p0对应代码中的list参数
    const-wide/16 v2, 0x0 // v2对应代码中的 sum变量

    .line 42
    .local v2, "sum":J
    invoke-virtual {p0}, Ljava/util/LinkedList;->iterator()Ljava/util/Iterator;

    move-result-object v0 // 这里是迭代器变量,上面一行代码执行LinkedList的iterator()方法的返回值

    .line 43
    .local v0, "it":Ljava/util/Iterator;, "Ljava/util/Iterator<Ljava/lang/String;>;"
    :goto_6 // 这里是标记循环开始的位置
    invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z

    move-result v1 // 将上一行代码中hasNext() boolean结果保存到v1变量中

    if-eqz v1, :cond_19 // 判断v1的值是否为true,如果不为true,则跳转到:cond_19标记的位置,即下面代码的return位置,即退出整个方法

    .line 44
    invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;

    move-result-object v1 // 取next()方法的结果,保存到v1变量中

    check-cast v1, Ljava/lang/String;

    invoke-virtual {v1}, Ljava/lang/String;->length()I

    move-result v1 // 将字符串的长度赋值到v1变量中,这里有点类似python语法,可以理解成,所有的变量都是object,不同类型的变量之间都可以赋值。v1变量之前的值已经不需要了,所以在这里会被重新覆盖成新值

    int-to-long v4, v1 // 将v1值转换成long值,并保存到v4变量中

    add-long/2addr v2, v4 // 将v2和v4的值相加,并保存到v2中,这里的v2在上面对应的sum变量

    goto :goto_6 // 跳转到循环开始的位置

    .line 47
    :cond_19
    return-wide v2
    .end method

    对比上面两个方法的smali代码,发现foreach循环最终编译成迭代器循环的代码。这就解释了为什么两种循环方式测试的消耗时间基本一致。其实也可以通过class字节码工具转换成java代码就可以看到,原来的foreach循环没有了,换成了迭代器循环。

    3.2 ArrayList测试代码的字节码

    ArrayList的迭代器遍历和foreach遍历的smali代码与上面LinkedList循环部分的代码基本一致,下面只贴出普通for循环部分的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    .method private static testArrayListFor(Ljava/util/ArrayList;)J
    .registers 7
    .annotation system Ldalvik/annotation/Signature;
    value = {
    "(",
    "Ljava/util/ArrayList",
    "<",
    "Ljava/lang/String;",
    ">;)J"
    }
    .end annotation

    .prologue
    .line 72
    .local p0, "list":Ljava/util/ArrayList;, "Ljava/util/ArrayList<Ljava/lang/String;>;"
    const-wide/16 v2, 0x0

    .line 73
    .local v2, "sum":J
    const/4 v0, 0x0 // v0是循环下标i

    .local v0, "i":I
    :goto_3
    invoke-virtual {p0}, Ljava/util/ArrayList;->size()I // size大小

    move-result v1

    if-ge v0, v1, :cond_18 // 如果v1大于v0,则跳转到:cond_18标记的位置,即return位置

    .line 74
    invoke-virtual {p0, v0}, Ljava/util/ArrayList;->get(I)Ljava/lang/Object; // 调用p0的get方法,参数值是v0

    move-result-object v1

    check-cast v1, Ljava/lang/String;

    invoke-virtual {v1}, Ljava/lang/String;->length()I

    move-result v1

    int-to-long v4, v1

    add-long/2addr v2, v4

    .line 73
    add-int/lit8 v0, v0, 0x1 // v0值加1,并保存到v0中

    goto :goto_3 // 跳转到:goto_3位置,重新开始循环

    .line 77
    :cond_18
    return-wide v2
    .end method

    通过上面的代码发现,普通for循环遍历,并没有使用迭代器的方式进行遍历。

    3.3 数组测试代码的字节码

    对于数组的遍历,两种方式的smali代码基本一致。也就是说foreach循环的代码编译成了普通的for i循环。代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    .method private static testArrayFor([Ljava/lang/String;)J
    .registers 7
    .param p0, "list" # [Ljava/lang/String;

    .prologue
    .line 52
    const-wide/16 v2, 0x0

    .line 53
    .local v2, "sum":J
    const/4 v0, 0x0

    .local v0, "i":I
    :goto_3
    array-length v1, p0

    if-ge v0, v1, :cond_11

    .line 54
    aget-object v1, p0, v0

    invoke-virtual {v1}, Ljava/lang/String;->length()I

    move-result v1

    int-to-long v4, v1

    add-long/2addr v2, v4

    .line 53
    add-int/lit8 v0, v0, 0x1

    goto :goto_3

    .line 57
    :cond_11
    return-wide v2
    .end method

    .method private static testArrayForeach([Ljava/lang/String;)J
    .registers 9
    .param p0, "list" # [Ljava/lang/String;

    .prologue
    .line 38
    const-wide/16 v2, 0x0

    .line 39
    .local v2, "sum":J
    array-length v4, p0 // 取p0数组的长度,保存到v4变量中

    const/4 v1, 0x0 // v1是下标

    :goto_4
    if-ge v1, v4, :cond_11

    aget-object v0, p0, v1 // 取p0的v1位置的值,并保存到v0变量中

    .line 40
    .local v0, "s":Ljava/lang/String;
    invoke-virtual {v0}, Ljava/lang/String;->length()I

    move-result v5

    int-to-long v6, v5

    add-long/2addr v2, v6

    .line 39
    add-int/lit8 v1, v1, 0x1

    goto :goto_4

    .line 43
    .end local v0 # "s":Ljava/lang/String;
    :cond_11
    return-wide v2
    .end method

    四、总结

    通过上面的代码对比分析发现:

    • 1.对于集合中的类,使用foreach循环遍历时,使用的是对应迭代器进行遍历;
    • 2.对于数组的foreach遍历,最终会被转换成普通的for循环。

    既然如此,使用foreach循环的好处也体现出来了。编译器会自动将集合转换成迭代器进行遍历,省去了手写迭代器循环的麻烦,简单明了。数组遍历同样也是简化代码,所以如果不需要对集合数组中的元素进行修改,建议优先使用foreach循环,避免犯一些低级问题(如,使用普通for i循环来遍历LinkedList,从而导致性能低下)。

    文章目录
    1. 1. 〇、前言
    2. 2. 一、集合遍历测试数据对比
      1. 2.0.1. 1.1 LinkedList循环测试数据对比
      2. 2.0.2. 1.2 ArrayList循环测试数据对比
  • 3. 二、数组遍历测试数据对比
  • 4. 三、从字节码看foreach循环
    1. 4.0.1. 3.1 LinkedList测试代码的字节码
    2. 4.0.2. 3.2 ArrayList测试代码的字节码
    3. 4.0.3. 3.3 数组测试代码的字节码
  • 5. 四、总结