1、以下一段再平常不过的遍历代码,但是与我一样,好多新手都会在这个地方出问题,例如
for(int i=0; i<list.size();i++) // 执行;
之前我在刚工作的时候在这个地方犯错,我们在自测的时候都没问题。初始化数据之后给客户演示时,突然蹦出个空指针异常NullPointerException。
于是我们通过debug发现,这个list为空,平常我们开发都会手工插入一些数据到表,自然不会报空,演示的时候数据表为空,自然就报错了。
改进:
for(int i=0; list!=null&&i<list.size(); i++) //执行;
不会报错了吧!
但是有经验的开发人员会发现for循环,每次都会去对循环条件进行判断,于是继续改进
if(list!=null && !list.isEmpty()) for(int i=0; i<list.size(); i++) // 执行;
还有问题吗
我们会发现循环还是每次都会去执行list.size();方法,list.size()方法也就是一句代码return size;看似简单的一句代码,但是我们知道每次执行一个方法JVM都会为该方法申请一个栈帧,过程虽短,但是也是一项不小的开销。
于是继续改进
if(list!=null && !list.isEmpty()) for(int i=0,len=list.size(); i<len; i++) // 执行;
这样效率真的会高一些吗,实验是最好的答案
List<Integer> list = new ArrayList<Integer>(); for(int i=0; i<30000000;i++) list.add(i); // 普通循环 long t1 = System.currentTimeMillis(); for(int j=0; j<100; j++) for(int i=0; i<list.size(); i++) { Integer it1 = list.get(i); } System.out.println(System.currentTimeMillis()-t1); // 改进len t1 = System.currentTimeMillis(); for(int j=0; j<100; j++) for(int i=0, len=list.size(); i<len; i++) { Integer it2 = list.get(i); } System.out.println(System.currentTimeMillis()-t1); // foreach方式遍历 t1 = System.currentTimeMillis(); for(int j=0; j<100; j++) for(Integer it4: list) { } System.out.println(System.currentTimeMillis()-t1); 结果 普通for结果 22335 使用len结果 14369 使用foreach结果 29163
经过多次测试发现,普通for结果比len会多出三分之一左右,foreach效果比普通for会略差,而且经过多次测试发现一个有趣的现象,循环位于不同的位置,结果也会有所差距,貌似JVM对循环过的集合会进行优化,下一次再次循环这个集合,速度会有所加快,这个大家也可以去做一下实验,集合过大的时候可能报堆溢出,可以通过修改。-Xms128M -Xmx512M
PS:以上遍历方法不考虑多线程不会有什么问题,如果在多线程下遍历将len提出来,就容易出错了,因为len就获取一次。
2、接下来考虑一个问题,多线程下需要同步遍历某个公共集合,如何实现
我们可能会想到用同步块synchronized
synchronized(list) { if(list!=null && !list.isEmpty()) for(int i=0,len=list.size(); i<len; i++) 执行; }
这样的代码真的好吗?我们可以想一想,多线程的情况下,你将公共的list一锁,其他的线程想插入list删除list只能干等着,更严重的是如果你在遍历的时候还执行一些长时间的操作,例如关闭连接或者启动监听等等。
于是我们就想如何能够让等待的时间尽量的短,我们完全可以先将集合克隆下来(至于深浅克隆根据具体情况而论),再去执行,当然如果循环执行时间很短,完全没这个必要。
LifecycleListener[] interested = null; // 用克隆减少时间,因为如果一个一个去触发事件响应,得使用大量时间,而又因为线程锁的存在,使得其他线程必须继续等待 synchronized (listeners) { interested = (LifecycleListener[])listeners.clone(); } // 逐个触发事件响应 for(int i=0; i<interested.length; i++) { interested[i].lifecycleEvent(event); }
为什么上面没有使用len中间变量,因为数组的length并不会占用执行时间。