前几天看到群里有人发了一篇关于java foreach循环(增强for循环)
测试的文章。文章没细看,从别人的发的截图来看是将LinkedList使用 for i
循环与foreach循环测试的数据做对比,然后得出foreach循环惊人的性能。猜写这文章的人应该是一个初学者,如果是我,会用迭代器来遍历LinkedList,然后再与foreach循环的数据作对比。因为LinkedList是双链表结构,并不适合使用 for i
循环这种方式进行遍历,实际写代码时也不会这么用。那foreach循环出现是为了解决什么问题?
使用的测试环境是jdk: 1.8.0_251,CPU:i7-9750H。
对于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循环两者不相上下。
同样测试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循环的代码有什么不同。
下面就以 smali
格式来查看字节码,因为 smali
这种方式跟java语法类似,感觉比不停的出栈、入栈要容易理解。
查看的方法是下载 jadx
工具,直接将class文件拖入到打开的窗口中,在右边的窗口中的左下角有一个 代码
和 smali
切换的Tab,点击 smali
就可以看到smali格式的代码。
下面只贴出核心部分的代码,想看完整的代码,可以自己动手试下。
.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循环没有了,换成了迭代器循环。
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循环遍历,并没有使用迭代器的方式进行遍历。
对于数组的遍历,两种方式的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
通过上面的代码对比分析发现:
既然如此,使用foreach循环的好处也体现出来了。编译器会自动将集合转换成迭代器进行遍历,省去了手写迭代器循环的麻烦,简单明了。数组遍历同样也是简化代码,所以如果不需要对集合数组中的元素进行修改,建议优先使用foreach循环,避免犯一些低级问题(如,使用普通 for i循环
来遍历LinkedList,从而导致性能低下)。