转载

通过java字节码分析java foreach循环

前几天看到群里有人发了一篇关于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遍历的测试代码如下所示:

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;
    }

}

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

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

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

1.2 ArrayList循环测试数据对比

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

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;
    }

}

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

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

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

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

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

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;
    }

}

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

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测试代码的字节码

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

.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循环部分的代码:

.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循环 。代码如下所示:

.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,从而导致性能低下)。

原文  http://www.jacpy.com/2020/06/20/analyze-java-foreach-by-bytecode.html
正文到此结束
Loading...