For-each 和 Index-for 循环最佳实践
for-each 循环优先于传统的for循环-- Joshua Blocha hand-written counted loop is better than for the enhanced loop. -- jackiemliu
本文仅限于 ArrayList,LinkedList 不在讨论话题内。
首先,相信大家对于这两种循环都很熟悉:
// foreach
for (Box box : boxList)
box.id++;
// index for(记住保存len,否则每次都需要调用boxList.size())
for (int index = 0,len = boxList.size(); index < len; index++)
boxList.get(index).id++;复制代码
但两者何时使用,性能和效率怎么权衡,又成为了新的问题!
因此这篇文章就主要是针对这两种循环方式和 Android 平台上的取舍做一些简单的分析。
最佳实践
先给出最佳实践,原因后面进行分析:
1、优先使用 index-for 模式(Android Framework 推荐);
2、如果想在遍历过程中暴露出其它线程正在修改(ConcurrentModificationException)的问题,请使用 for-each 模式.
深入分析
Iterate 模式
for each这种写法其实是一个语法糖,其实
for(Box box:boxList)
等同于 for(Iterator var1 = boxList.iterator(); var1.hasNext();var1.next())
可以看到这种模式会去额外生成一个 Iterator 对象,所以相较于 Index 模式而言,它会额外使用一些内存。
在 Android 平台,内存资源是极为有限的,如果只是单层的循环还算 OK,但是如果是多层循环,
或者是隐式的多层循环中使用 Iterate 模式,可想而知内存会临时分配很多个变量。
例如:
// 显式多层循环
for (Box box : boxList)
for (InnerBox innerBox : box)
; // do something
// 隐式的多层循环(View 的 onDraw 方法)
void onDraw(Canvas c){
for (Box box : boxList)
; // do something
}复制代码
以上这两种情况可能来说就会分配过多的临时对象,导致内存不足进行 GC ,从而影响 App 流畅度。
除此之外,在迭代的过程中,会去调用 Iterator 的 next()
,这里我以 ArrayList 为例:
public E next() {
checkForComodification(); // 检查是否在遍历过程中有人修改了列表
int i = cursor;
if (i >= size) // 检查下标是否合法
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) // 检查下标是否合法
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i]; // 返回对象
}复制代码
相比与Index 模式,它增加了很多检查,所以会带来一定的开销,但也是一种特性(检查遍历中是否有人修改)。
Index 模式
大家最常见的可能是:
for(int index = 0; index < boxList.size();index++)
; // do something复制代码
但这种方式其实并不够理想,因为每次循环时,都会去调用 List.size()。
所以我们可以将其保存下来:
for(int index = 0,len = boxList.size(); index < len;index++)
; // do something复制代码
相对而言,保存 len 的方法更快,尤其在 Effective Java 中的范例:
for(int index = 0,n = expensiveComputation(); index < n;index++)
; // do something复制代码
如果一个方法耗时较多且结果不会改变,那么可以用一个临时变量充当缓存。
性能比较
速度
一开始我还自己在电脑上做了个小 demo,然而发现 Android Team 已经做了一个更具代表性的(多次取平均值),这里直接借用吧:
mark这里的案例是 400,000 随机的 Integer,每种模式都跑 10 次,去掉最大最小值后取平均结果。
可以很明显看出 Index 模式的耗时完美胜出,Yeah |ω・)
内存占用
由于之前介绍了 Iterate 模式会初始化一个 Iterator 对象,所以它的内存占用肯定多于 Index 模式。
总结
0、语法糖可能很甜,但也可能有隐藏的性能损耗(e.g lambda 表达式会增加运行时开销)
1、从语法上而言,foreach 这种更简洁,也更加地隐藏了细节(语法糖),但也缺失了某些特性。
简单来说,尽可能将 foreach 看成是一种 只读向后遍历。
在 Effective Java 中,介绍了无法使用的三种场景:
- 过滤——如果需要在遍历过程中删除元素。如果在 foreach 中删除,会引发并发修改的异常。
- 转换——如果需要在遍历过程中对元素进行替换。如果在 foreach 中替换,例如 o = new Object(); 其实并没有改变列表中的值。
- 平行迭代——如果需要灵活更改遍历的顺序时。
2、开发 Android 的时候,尽可能放弃使用 foreach ,减少内存压力。
3、如果嫌弃 Index 模式的模板代码太麻烦,可以试试 Live Templates 中自带的 itli ,一键生成循环代码哦~
4、在写 Index 模式的时候,尽可能去保存 list.size(),虽然 JIT 有可能会进行优化,但这种方式可以更加保险。
5、性能优化不只是整体架构或者类库的优化,也要从平时点滴做起。
最后,推荐大家看看: Performance Tips
参考:
To Index or Iterate?
Use Enhanced For Loop Syntax