前几天看到群里有人发了一篇关于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,从而导致性能低下)。