Java中使用for而不是forEach遍历List的10大理由
首发公众号:【赵侠客】
引言
我相信作为一名java开发者你一定听过或者看过类似《你还在用for循环遍历List吗?》、《JDK8都10岁了,你还在用for循环遍历List吗?》这类鄙视在Java中使用for循环遍历List的水文。这类文章说的其实就是使用Java8中的Stream.foreach()
来遍历元素,在技术圈感觉使用新的技术就高大上,开发者们也都默许接受新技术的很多缺点,而使用老的技术或者传统的方法就会被人鄙视,被人觉得Low,那么使用forEach()
真的很高大上吗?它真的比传统的for
循环好用吗?本文就列出10大推荐使用for
而不是forEach()
的理由。
理由一、for性能更好
在我的固有认知中我是觉得for
的循环性能比Stream.forEach()
要好的,因为在技术界有一条真理:
越简单越原始的代码往往性能也越好
而且搜索一些文章或者大模型都是这么觉得的,可时我并没有找到专业的基准测试证明此结论。那么实际测试情况是不是这样的呢?虽然这个循环的性能差距对我们的系统性能基本上没有影响,不过为了证明for
的循环性能真的比Stream.forEach()
好我使用基准测试用专业的实际数据来说话。我的测试代码非常的简单,就对一个List<Integer> ids
分别使用for
和Stream.forEach()
遍历出所有的元素,以下是测试代码:
@State(Scope.Thread)
public class ForBenchmark {
private List<Integer> ids ;
@Setup
public void setup() {
ids = new ArrayList<>();
//分别对10、100、1000、1万、10万个元素测试
IntStream.range(0, 10).forEach(i -> ids.add(i));
}
@TearDown
public void tearDown() {
ids = new ArrayList<>();
}
@Benchmark
public void testFor() {
for (int i = 0; i <ids.size() ; i++) {
Integer id = ids.get(i);
}
}
@Benchmark
public void testStreamforEach() {
ids.stream().forEach(x->{
Integer id=x;
});
}
@Test
public void testMyBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(ForBenchmark.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(1)
.measurementIterations(1)
.mode(Mode.Throughput)
.build();
new Runner(options).run();
}
}
我使用ArrayList分对10、100、1000、1万,10万个元素进行测试,以下是使用JMH基准测试的结果,结果中的数字为吞吐量,单位为ops/s,即每秒钟执行方法的次数:
方法 | 十 | 百 | 千 | 万 | 10万 |
---|---|---|---|---|---|
forEach | 45194532 | 17187781 | 2501802 | 200292 | 20309 |
for | 127056654 | 19310361 | 2530502 | 202632 | 19228 |
for对比 | ↑181% | ↑12% | ↑1% | ↓1% | ↓5% |
从使用Benchmark基准测试结果来看使用for遍历List比Stream.forEach性能在元素越小的情况下优势越明显,在10万元素遍历时性能反而没有Stream.forEach好了,不过在实际项目开发中我们很少有超过10万元素的遍历。
所以可以得出结论:
在小List(万元素以内)遍历中for性能要优于Stream.forEach
理由二、for占用内存更小
Stream.forEach()会占用更多的内存,因为它涉及到创建流、临时对象或者对中间操作进行缓存。for 循环则更直接,操作底层集合,通常不会有额外的临时对象。可以看如下求和代码,运行时增加JVM参数-XX:+PrintGCDetails -Xms4G -Xmx4G
输出GC日志:
- 使用for遍历
List<Integer> ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList());
int sum = 0;
for (int i = 0; i < ids.size(); i++) {
sum +=ids.get(i);
}
System.gc();
//GC日志
[GC (System.gc()) [PSYoungGen: 392540K->174586K(1223168K)] 392540K->212100K(4019712K), 0.2083486 secs] [Times: user=0.58 sys=0.09, real=0.21 secs]
从GC日志中可以看出,使用for遍历List在GC回收前年轻代使用了392540K,总内存使用了392540K,回收耗时0.20s
- 使用stream
List<Integer> ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList());
int sum = ids.stream().reduce(0,Integer::sum);
System.gc();
//GC日志
[GC (System.gc()) [PSYoungGen: 539341K->174586K(1223168K)] 539341K->212118K(4019712K), 0.3747694 secs] [Times: user=0.55 sys=0.83, real=0.38 secs]
从GC日志中可以看出,回收前年轻代使用了539341K,总内存使用了539341K,回收耗时0.37s ,从内存占用情况来看使用for会比Stream.forEach()占用内存少37%,而且Stream.foreach() GC耗时比for多了85%。
理由三、for更易控制流程
我们使用for遍历List可以很方便的使用break
、continue
、return
来控制循环,而使用Stream.forEach在循环中是不能使用break
、continue
,特别指出的使用return
是无法中断Stream.forEach循环的,如下代码:
List<Integer> ids = IntStream.range(1,4).boxed().collect(Collectors.toList());
ids.stream().forEach(i->{
System.out.println(""+i);
if(i>1){
return;
}
});
System.out.println("==");
for (int i = 0; i < ids.size(); i++) {
System.out.println(""+ids.get(i));
if(ids.get(i)>1){
return;
}
}
输出:
forEach-1
forEach-2
forEach-3
==
for-1
for-2
从输出结果可以看出在Stream.forEach中使用return后循环还会继续执行的,而在for循环中使用return将中断循环。
理由四、for访问变量更灵活
这点我想是很多人在使用Stream.forEach中比较头疼的一点,因为在Stream.forEach中引用的变量必须是final类型,也就是说不能修改forEach循环体之外的变量,但是我们很多业务场景就是修改循环体外的变量,如以下代码:
Integer sum=0;
for (int i = 0; i < ids.size(); i++) {
sum++;
}
ids.stream().forEach(i -> {
//报错
sum++;
});
像上面的这样的代码在实际中是很常见的,sum++在forEach中是不被允许的,有时为了使用类似的方法我们只能把变量变成一个引用类型:
AtomicReference<Integer> sum= new AtomicReference<>(0);
ids.stream().forEach(i -> {
sum.getAndSet(sum.get() + 1);
});
所以在访问变量方面for会更加灵活。
理由五、for处理异常更方便
这一点也是我使用forEach比较头疼的,在forEach中的Exception必须要捕获处理,如下代码:
public void testException() throws Exception {
List<Integer> ids = IntStream.range(1, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
//直接抛出Exception
System.out.println(div(i, i - 1));
}
ids.stream().forEach(x -> {
try {
//必须捕获Exception
System.out.println(div(x, x - 1));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private Integer div(Integer a, Integer b) throws Exception {
return a / b;
}
我们在循环中调用了div()方法,该方法抛出了Exception,如果是使用for循环如果不想处理可以直接抛出,但是使用forEach就必须要自己处理异常了,所以for在处理异常方面会更加灵活方便。
理由六、for能对集合添加、删除
在for循环中可以直接修改原始集合(如添加、删除元素),而 Stream 不允许修改基础集合,会抛出 ConcurrentModificationException,如下代码:
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
if(i<1){
ids.add(i);
}
}
System.out.println(ids);
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2.stream().forEach(x -> {
if(x<1){
ids2.add(x);
}
});
System.out.println(ids2);
输出:
[0, 1, 2, 3, 0]
java.util.ConcurrentModificationException
如果你想在循环中添加或者删除元素foreach是无法完成了,所以for处理集合更方便。
理由七、for Debug更友好
Stream.forEach()使用了Lambda表达示,一行代码可以搞定很多功能,但是这也给Debug带来了困难,如下代码:
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
System.out.println(ids.get(i));
}
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2.stream().forEach(System.out::println);
以下是DeBug截图:
我们可以看出使用for循环Debug可以一步一步的跟踪程序执行步骤,但是使用forEach却做不到,所以for可以更方便的调试你的代码,让你更快捷的找到出现问题的代码。
理由八、for代码可读性更好
Lambda表达示属于面向函数式编程,主打的就是一个抽象,相比于面向对象或者面向过程编程代码可读性是非常的差,有时自己不写的代码过段时间后自己都看不懂。就比如我在文章《解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!》一文中使用函数式编程写了一个Tree工具类,我们可以对比一下面向过程和面向函数式编程代码可读性的差距:
- 使用for面向过程编程代码:
public static List<MenuVo> makeTree(List<MenuVo> allDate,Long rootParentId) {
List<MenuVo> roots = new ArrayList<>();
for (MenuVo menu : allDate) {
if (Objects.equals(rootParentId, menu.getPId())) {
roots.add(menu);
}
}
for (MenuVo root : roots) {
makeChildren(root, allDate);
}
return roots;
}
public static MenuVo makeChildren(MenuVo root, List<MenuVo> allDate) {
for (MenuVo menu : allDate) {
if (Objects.equals(root.getId(), menu.getPId())) {
makeChildren(menu, allDate);
root.getSubMenus().add(menu);
}
}
return root;
}
- 使用forEach面向函数式编程代码:
public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
}
private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> children) {
return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> children.accept(x, makeChildren(x, allData, parentCheck, children))).collect(Collectors.toList());
}
对比以上两段代码,可以看出面向过程的代码思路非常的清晰,基本上可以一眼看懂代码要做什么,反观面向函数式编程的代码,我想大都人一眼都不知道代码在干什么的,所以使用for的代码可读性会更好。
理由九、for更好的管理状态
for循环可以轻松地在每次迭代中维护状态,这在Stream.forEach中可能需要额外的逻辑来实现。这一条可理由三有点像,我们经常需要通过状态能控制循环是否执行,如下代码:
boolean flag = true;
for (int i = 0; i < 10; i++) {
if(flag){
System.out.println(i);
flag=false;
}
}
AtomicBoolean flag1 = new AtomicBoolean(true);
IntStream.range(0, 10).forEach(x->{
if (flag1.get()){
flag1.set(false);
System.out.println(x);
}
});
这个例子说明了在使用Stream.forEach时,为了维护状态,我们需要引入额外的逻辑,如使用AtomicBoolean,而在for循环中,这种状态管理是直接和简单的。
理由十、for可以使用索引直接访问元素
在某些情况下,特别是当需要根据元素的索引(位置)来操作集合中的元素时,for就可以直接使用索引访问了。在Stream.forEach中就不能直接通过索引访问,比如我们需要将ids中的数字翻倍:
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
ids.set(i,i*2);
}
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2=ids2.stream().map(x->x*2).collect(Collectors.toList());
我们使用for循环来遍历这个列表,并在每次迭代中根据索引i来修改列表中的元素。这种操作直接且直观。而使用Stream.foreach()不能直接通过索引下标访问元素的,只能将List转换为流,然后使用map操作将每个元素乘以2,最后,我们使用Collectors.toList()将结果收集回一个新的List。
总结
本文介绍了在实际开发中更推荐使用for循环而不是Stream.foreach()来遍历List的十大理由,并给出了具体的代码和测试结果,当然这并不是说就一定要使用传统的for循环,要根据自己的实际情况来选择合适的方法。通过此案件也想让读者明白在互联网世界中你所看到的东西都是别人想让你看到的,这个世界是没有真相的,别人想让你看到的就是所谓的”真相“,做为吃瓜群众一定不能随波逐流,要有鉴别信息真假的能力和培养独立思考的能力。
来源:juejin.cn/post/7416848881407524902