Java中List遍历的⼏个问题
之前在项⽬中引⼊Lambda表达式后,最近就把之前的代码改为Lambda表达式,中间遇到了⼀个⼩插曲就是List的在调⽤Stream的forEach()中使⽤return 、break、continue关键字的问题;加上最近⼀直关注的“码农每⼀题”于是⾃⼰回顾⼀下List的基础温故⽽知新了;
⼀、List⼏种遍历⽅式的问题
Java 中常见的⼏种遍历⽅式⽅式:
1.loop without size / for(i=0;i<expr.length-1;i++)
2.foreach/ for(T item:expr)
3.Iterator/迭代器
4.Stream.forEach()
5.parallelStream().forEach();
问题1:foreach增强for循环中修改List中element的值操作⽆效;
⽰例代码:
public static void main(String[] args) {
int size = 1000;
String s[] = new String[]{"qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds"};
List<String> asList = Arrays.asList(s);
for (String t : asList) {
if (t.length() <= 4) {
System.out.println(t);
t = "1122";
}
}
for (String tt : asList){
System.out.println("==== :"+tt);
}
}
//程序运⾏结果
asd
qwds
==== :qwqwe
==== :frsgdf
==== :asd
==== :dfsfuytrd
==== :qwds
问题缘由:
foreach遍历JDK5.0增加的增强for循环,foreach在遍历过程中是通过⼀个临时变量,记录遍历到的当前List中的element,所以在foreach中操作的对象是指向临时变量的,⽽不是List中的element实例对象的地址,结果⾃然就只是修改临时变量的值并没修改List中的element,所以才会出现:foreach增强for循环中修改List中element的值是⽆效的问题;
解决办法:
改⽤loop without size实⾏;
问题2:Iterator迭代时,调⽤List集合对象remove(Object o)时,抛出Exception;
⽰例代码:
java streampublic static void main(String[] args) {
List<String> asList = new ArrayList<>();
asList.add("qwqwe");
asList.add("frsgdf");
asList.add("asd");
asList.add("dfsfuytrd");
asList.add("qwds");
Iterator<String> iterator = asList.iterator();
String next;
while (iterator.hasNext()) {
next = ();
if(next.length()<=4){
}
}
}
//运⾏结果
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.(ArrayList.java:851)
at Main.main(Main.java:31)
问题缘由:
这个问题是和Iterator的实现⽅式有关系的,以ArrayList为例,在ArrayList中.iterator()其实是通过⼯⼚模式在内部new出来⼀个Iterator 对象,⽽且这个Iterator对象的size是按照它被创建时List的.size()⼤⼩创建的,如果在iterator()中调⽤List的remove⽅法,这就会导致Iterator的size⼤于List的size,进⽽发
⽣IndexOutOfBoundsException越界异常(List中改为抛出ConcurrentModificationException,可参考()函数);
解决办法:
1.如果list中需要删除⼀个element的操作的话可以的话,删除完成直接break;这样也可以节约时间和减⼩性能开销;
2.调⽤Iterator的remove()⽅法进⾏删除【在源码中可以看到在Iterator的remove()中同时也调⽤了List的remove(),这保持了List的size和
Iterator的size⼀致,避免出现越界异常;】
问题3:JDK8中Stream.forEach()遍历时return、break、continue关键字使⽤【parallelStream也存在这样问题】;
在JDK8中引⼊的Stream中利⽤forEach()遍历List中,发现break和continue两个关键字IDE会直接提⽰语法错误的,所以这连个关键字就直接可以pass了,直接看return吧;
⽰例代码:
public static void main(String[] args) {
List<String> list = Arrays.asList("qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds");
list.stream().forEach(e -> {
if (e.length() <= 4) {
System.out.println("----"+e);
return;
}
System.out.println(e);
});
}
//程序执⾏结果:
qwqwe
frsgdf
----asd
dfsfuytrd
----qwds
问题缘由:
在stream[parallelStream中也是⼀样的]中关键字return、break、continue关键字使⽤问题是和Java8中流Stream的设计有关系的,在Java8中引⼊的流的⽬的是提⾼并发执⾏效率即:Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进⾏各种⾮常便利、⾼效的聚合操作(aggregate operation),或者⼤批量数据操作 (bulk data operation);
可以理解为Stream操作的对象是Collection)集合对象⾃⾝⽽不是集合Collection中的element,⽽且Stream中的各个⽅法都是⼀旦开始执⾏就没有回头路,只能等待全部执⾏完成。
1.break和continue关键字在Sream中失效问题,个⼈理解为:Stream每次执⾏的是对整个集合为最⼩操作单元,⽽break和
continue是以集合Collection中的element为操作单元的,所以这两关键字在设计上就不是⼀个量级的,所以它们在Stream⾯前就失效了;
2 .return 在遍历结果来看其实充当了continue的⾓⾊,同样return在整个Java中的⽅法中充当了“急刹车和掉头返回”的功能,根据
上⾯的理解:在上述代码中return的对象只是Steam操作中的⼀个⼩⽀流element,所以return也只能对其中⽬标element进⾏刹车,并不能阻⽌其他element的继续,结果就是return在Stream中充当continue成了⼀个既成事实;
解决办法:
个⼈的观点是先搞清楚基本原理,根据实际需求灵活选择;
⼩插曲
刚开始看结果给⼈的感觉就是return充当了continue的⾓⾊,⽽且还是按照List的顺序执⾏的,菜鸡还
是百度了⼀下结果都说Java8中的stream是并发的数据量⼤的话就可能是出现乱序,于是赶紧⾃⼰测试了1000个String结果任然是按顺序打印的,⼜在CSDN中看到有⼈说String太简单,于是new了⼀个JavaBean结果还是按照顺序打印,于是越发感觉⽹上"那些⼈是胡说",再者在源码看到stream和parallelStream⼆者差别时,更加确认stream是sequential有序的,⽽parallelStream才是parallel⽆序的;
⼆、List⼏种遍历⽅式的效率问题
Java⼀直被⼈诟病的就是效率问题了,所以最后咋能不简单的对⽐⼀下呢;
基础测试前准备问题
1.经验告诉我们是性能越差劲的设备[CPU、RAM]越能放⼤代码效率差异,所以选择在Android⼿机上进⾏测试;
2.测试选择ArrayList和LinkedList两种最长⽤到的List,同时也是两种不同数据结构的List进⾏验证;
3.测试Size分别选择size为50,500,1000,5000,10000,50000作为验证变量;
4.测试List遍历的对象为JavaBean【有String.int long三种基本类型,且每次遍历都是相同打印操作】;
5.测试过程中所有的遍历⽅式中操作完全相同;
6.测试过程中每次测试前杀死⼿机其他app,完成⼀次测试后杀死测试app等⼀⼩会尽可能消除内存影响;
测试结果为:
基本结论:
1.随着Size的逐渐变⼤parallelStream遍历的效率就越明显,在Size达到5000+以后parallelStream遍历时间基本上是其他遍历⽅式
的时间的⼀半 ;
2.根据测试结果,在JDK8之前⼏种遍历的⽅式中通过Size循环遍历效率最差,Iterator和foreach效率基本差不多,但是foreach代码更
简洁;
3.在parallelStream遍历中LinkedList的遍历效率明显优于ArrayList;这是和LinkedList的数据结构以及parallelStream的遍历逻辑
有关系的
4.JDK8中引⼊是stream在List的size在5000以下时遍历的时间由于其他遍历⽅式【parallelStream以外】这个结果不知道正确不;测试的⼏个问题:
1.在测试过程中发现同样的Size测试⼏次结果⼏乎每次都有细微的差异,个⼈分析认为是和测试时⼿机状态有关系,不同时间⼿机系统
内部不同操作导致CPU占⽤情况不相同导致的;
2.这个测试数据结果中并没有很明显体现出ArrayList和LinkedList相⽐在查询的中的优势:在foreach遍历⽅式中⼆者时间基本上没
有差异;这个有点不太明⽩是什么原因导致的,希望有哪位⼤佬答疑解惑。
【下⽂中已经指出问题根源和改进建议】
三、重要的补充
对这是⼀个重要的补充,是针对的上⽂中对List测试的⼀个重要补充。
在最近准备看⾯试题看到关于try catch性能影响时,看到的⼀篇博⽂中不正确测试后,于是赶紧写代码测试[重现之前测试会⾃相⽭盾的结果]验证之前测试⽅式的错误;于是解开了笔者在上⽂中测试结果困惑;由于本⼈是半路的码农,以⽬前个⼈的认知⽔平经过测试验证后,个⼈⼗分认同提出的问题和解决思路。
下⽂是在⼤量引⽤⽂中的内容同时夹杂少部分个⼈理解,如有错误纰漏望诸位及时指正;
1.测试中的问题
a、System.currentTimeMillis()和System.nanoTime()测测量程序运⾏耗时的不准确
⾸先System.currentTimeMillis()和System.nanoTime()得到的只是当前时间点,前后时间差只是两次调⽤代码的时间差;这中间不仅仅只有函数的运⾏时间还有线程抢占CPU资源时的等待时间,所以难以保证时间的准确性;
b、Java中JIT优化导致结果出现偏差;
在JVM中的JIT的JIT优化同样会导致结果出现偏差;
JIT: 在Java编程语⾔和环境中,即时编译器(JIT compiler,just-in-time compiler)是⼀个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。
热点代码(Hot Spot Code): 当虚拟机发现某个⽅法或代码块运⾏特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提⾼热点代码的执⾏效率,在运⾏时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进⾏各层次的优化,完成这项任务的正是JIT编译器。 在某些情况下,调整好的最佳化 JVM 效能可能超过⼿⼯的 C++ 或 C;
运⾏过程中会被即时编译器编译的“Hot Spot Code”有两类: 多次调⽤的⽅法、多次调⽤的循环体。
显然测试代码正是典型的:频繁的循环的循环体,JIT也增添了更⼤误差;
c、类加载时间和程序运⾏时间叠加
在⾸次run的时候类的加载时带来的时间误差;
2.正确的测试⽅式
a、不要使⽤System.currentTimeMillis()亦或者使⽤System.nanoTime() ;
这⾥说明⼀下,可能你会看到有些建议使⽤System.nanoTime()来测试,但是它跟System.currentTimeMillis()区别,仅仅在于时间的基准不同和精度不同,但都表⽰的是逝去的时间,所以对于测试执⾏时间上,并没有什么区别。因为都⽆法统计CPU真正执⾏时间。
b、推荐使⽤JProfiler性能测试⼯具
要测试cpu真正执⾏时间,这⾥推荐使⽤JProfiler性能测试⼯具,它可以测量出cpu真正的执⾏时间。具体安装使⽤⽅法可以⾃⾏google百度。因为这不是本⽂最终使⽤的测试⽅法,所以就不做详细介绍
了。但是你使⽤它来测试上⾯的代码,⾄少可以排除等待CPU消耗的时间对于后两者,需要加⼊Warmup(预热)阶段。
预热阶段就是不断运⾏你的测试代码,从⽽使得代码完成初始化⼯作(类加载),并⾜以触发JIT编译机制。⼀般来说,循环⼏万次就可以预热完毕。
那是不是做到以上两点就可以了直抵真相了?⾮常不幸,并没有那么简单,JIT机制和JVM并没有想象的这么简单,要做到以下这些点你才能得到⽐较真实的结果。建议参考排名第⼀的答案;
还可以参考Java theory and practice:
认真看完这些,你就会发现,要保证microbenchmark结果的可靠,真不是⼀般的难
四、附上错误的测试代码和⼿机参数
测试Android⼿机:华为畅享7(SLA-AL00/2GB RAM/全⽹通);
测试完整代码:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.button_a)
Button buttonA;
private static int size = 1000;
@BindView(R.id.size)
EditText dt_size;
@BindView(R.id.button_b)
Button buttonB;
@BindView(View)
TextView textView;
private ArrayList<TestBean> as;
private LinkedList<TestBean> ls;
@Override
protected void onCreate(Bundle savedInstanceState) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论