您的位置:首页 > 其它

for、for-each以及for-each(lambda)循环的执行效率研究,以及额外的收获

2019-01-04 18:02 141 查看

之前对循环操作基本上是能用lambda就用lambda,不行再用for-each,最后再考虑for,毕竟是优先选择书写简便的循环方式。然而不经意间看到一篇关于三种类型的循环执行效率的博客,才发现自己之前只顾着书写简便,而没有考虑到效率问题,于是敲了段代码研究研究。

List<Person> arrayList = new ArrayList<>();
Person person;
for (int i = 0; i < 10000000; i++) {
person = new Person();
person.setI(i);
person.setStr(String.valueOf(i));
arrayList.add(person);;
}
/*for*/
long start = System.currentTimeMillis();
for (int i = 0, len = arrayList.size(); i < len; i++) {
int test = arrayList.get(i).getI();
}
System.out.println("arrayList-for:" + (System.currentTimeMillis() - start));
/*foreach*/
start = System.currentTimeMillis();
for (Person p : arrayList) {
int test = p.getI();
}
System.out.println("arrayList-foreach:" + (System.currentTimeMillis() - start));
/*lambda*/
start = System.currentTimeMillis();
arrayList.forEach(p -> {
int test = p.getI();
});
System.out.println("arrayList-lambda:" + (System.currentTimeMillis() - start));

执行多次后结果分析:
在对拥有一千万元素的集合进行遍历时,for和foreach耗时差不多,lambda也就多个一百毫秒左右,所以在实际应用中,还是放心的使用foreach或lambda吧!

but,这只是对ArrayList的分析,当我将ArrayList换成LinkedList之后,结果却截然不同,在for循环下LinkedList的执行效率低到了令人发指的地步!这是因为ArrayList和LinkedList虽然都可以根据下标获取元素,但ArrayList本质上是一个数组,可以直接根据下标获取元素,复杂度是O(1),而LinkedList是一个链表,每次必须先循环遍历到对应的下标位置后才能获取元素,复杂度是O(index),自然消耗的时间远远大于ArrayList。

所以如果是LinkedList,最好不要用for循环进行遍历

到了这里,我又产生了新的疑问,foreach又叫增强for循环,也就是将for循环的书写简化,本质上不还是for循环吗?那为什么LinkedList用foreach遍历速度却这么快呢?
抱着这个疑问,我又在网上查询foreach相关资料,发现原来我对foreach的理解错了! foreach 在遍历集合时,会有选择的转换为for循环或者Iterator迭代器,而LinkedList是使用迭代器遍历而不是for循环,相当于按照顺序从首到尾逐条获取集合中的元素,这样不需要每次循环遍历到对应下标位置了,复杂度也是O(1)所以能达到快速遍历的效果。

说到这,又出现一个问题,foreach是根据什么来选择转换为for循环还是Iterator迭代器呢?
通过ArrayList和LinkedList的源代码我们可以发现,ArrayList实现了一个叫做RandomAccess的接口,而这个叫做随机访问的接口里空空如也,那么显而易见,这接口的唯一作用就是作为一个标记,表示实现了这个接口的类拥有随机访问的属性,而ArrayList正好符合这个条件。
通过查询JAVA API文档中关于RandomAccess的介绍:

List 实现所使用的标记接口,用来表明其支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。

将操作随机访问列表的最佳算法(如 ArrayList)应用到连续访问列表(如 LinkedList)时,可产生二次项的行为。如果将某个算法应用到连续访问列表,那么在应用可能提供较差性能的算法前,鼓励使用一般的列表算法检查给定列表是否为此接口的一个 instanceof,如果需要保证可接受的性能,还可以更改其行为。

现在已经认识到,随机和连续访问之间的区别通常是模糊的。例如,如果列表很大时,某些 List 实现提供渐进的线性访问时间,但实际上是固定的访问时间。这样的 List 实现通常应该实现此接口。实际经验证明,如果是下列情况,则 List 实现应该实现此接口,即对于典型的类实例而言,此循环:

for (int i=0, n=list.size(); i < n; i++)
list.get(i);

的运行速度要快于以下循环:

for (Iterator i=list.iterator(); i.hasNext(); )
i.next();

同时阅读网上一些相关博客也证实了这一点,foreach会根据集合类是否实现了RandomAccess接口来选择用for循环还是Iterator迭代器来遍历该集合,这样能够保证集合遍历的高效运行。

总结:一开始只是出于好奇验证下for与foreach以及lambda执行效率的问题,没想到却误打误撞学习了很多额外的知识,有了很多的收获,也算是学习JAVA的乐趣所在了。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: