for、for-each以及for-each(lambda)循环的执行效率研究,以及额外的收获
之前对循环操作基本上是能用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的乐趣所在了。
- C++新特性之lambda表达式在for_each循环中的使用及for循环新写法
- 执行原因【菜鸟笔记】Ubuntu系统shellscript中 关于for循环以及declare出错的原因
- for循环中++i与i++的执行效率与区别
- for()循环中递减比递增会少执行一次判断(i++循环与i-–循环的执行效率)
- 关于for循环的执行效率问题
- swift中c风格的for循环执行效率
- 关于for循环与for-each的效率比较
- 测试递归与循环(这里用for)的执行效率与系统开销
- javascript中for、each以及foreach的效率对比
- 关于for循环的执行效率的问题
- C语言For循环的执行过程以及变量自增自减过程。
- PHP 数组的遍历的几种方式(以及foreach与for/while+each效率的比较)
- for循环执行效率要比while循环高
- 循环执行时间JQuery 性能分析系列一 —— for与each性能比较
- 列表推导式对比For循环执行效率
- for循环执行效率
- 关于执行两个for循环效率注意点
- for循环执行效率
- for循环语句以及break和continue的使用
- for循环执行过程验证