跳至主要內容

forEach 还是 map?

JavaDaily

forEach 还是 map?

背景

遍历一个集合,在里面执行某种操作后,再依次返回每一个元素,常见的实现方式有:

List<Type> result = new ArrayList<>();
list.forEach(src -> {
    Type target = BeanUtils.copyProperties(src, target);
    //省略代码
    result.add(target);
});
List<Type> result = list.map(src -> {
    Type target = BeanUtils.copyProperties(src, target);
    //省略代码
   return target;
});

两种方式看起来没多大差别啊,到底用哪种呢?

结论

先说结论:根据《Effective Java》(第三版),forEach 只用于消费数据的场景,并不应该用于计算、累加,故上述代码应该使用 map。

原文如下:

红字处翻译:forEach 仅适用于输出 stream 里的计算结果,并不适合执行计算。

解析

为更好地理解上述结论,需要先理解以下内涵:

Stream 的引入,不仅带来新的语法,也带来了函数式编程的思维。

这里最重要的一点就是:编写纯函数(pure function),不造成副作用(side-effect)。

纯函数可以用数学中的函数映射来理解:y = f(x)

  • 给定 x,能唯一确定 y
  • 无论函数调用几次、在何处调用,上述结果都不会变化

无副作用意思是:调用函数后,不会对函数作用域以外的变量造成影响。而纯函数,一定是无副作用的。

前文使用 forEach 的代码,其实是造成了副作用的:

List<Type> result = new ArrayList<>();
list.forEach(src -> {
    Type target = BeanUtils.copyProperties(src, target);
    result.add(target); //side effect
});

很简单的一个识别方法:再调用一次 forEach,result 的结果还是期望的结果吗?显然不是。

但如果 map 方法呢?再调用一次,结果不变!

List<Type> result = list.map(src -> {
    Type target = BeanUtils.copyProperties(src, target);
   return target; // side effect free
});

类似的,修改函数入参,也不是纯函数:

List<String> ids = new ArrayList<>();

collectIds(data, ids); // 在这里填充 ids!

int size = ids.size(); 

上面的 collectIds函数是令人讨厌的——写代码的人懒得写函数返回时,直接修改函数入参,给后面维护的人留下隐患。

当然,如果一定要用纯函数来看待问题,未免过于理想化,因为有时要执行这样的代码:

list.forEach(v -> {
    myService.save(v);
});

虽然上述代码并没影响到函数作用域以外的代码变量,但 myService 会把数据持久化,站在整个应用的角度讲,仍然造成了副作用。

但上述代码可以接受的。因此,建议记住 forEach 只用于消费数据,不用于计算及返回,就不容易混淆。

实战

当然,上述的讨论还是比较偏理论的,我们来看一下实际项目中,滥用 forEach 可能导致可读性较差的问题。

代码一开始,是简单清晰的:

// 一个只有20行的函数
void myFunction(List<Node> nodes, List<Edge> edges) {
    Type1 var1;
    Type2 var2;
    
    nodes.forEach(node -> {
        // 修改 var1
    })
      
    edges.forEach(edge -> {
        // 修改 var2
    })
}

然而,业务会变化,逻辑会复杂,代码也要修改。而上述在 forEach 中修改变量的行为,罪恶的根源在于,它在向后来修改代码的人发出邀请:新增的逻辑,写在这个 forEach 里面就好了!

当仅在 forEach 添加代码就能完成任务的时候,很难有人能抵抗这种诱惑,于是就会变成:

// 一个超过50行的函数
void myFunction(List<Node> nodes, List<Edge> edges) {
    Type1 var1;
    Type2 var2;
    Type3 var3;
    Type4 var4;
    Type5 var5;
    
    nodes.forEach(node -> {
        // 里面有多个 if-else
        // 修改了 var1, var2
        // 有可能修改 var5
    })
      
    edges.forEach(edge -> {
        // 里面有多个 if-else
        // 修改了 var3, var4
        // 有可能修改 var5
    })
}

写代码一时爽,维护火葬场。上面的代码,将是维护的噩梦!

并且,如果维护者只想知道函数的整体逻辑,由于变量穿插、隐藏在 forEach 内部,维护者不得不在各种 if-else 里面追踪变量,很容易就陷入不必要的细节中。

如果换个方式来写呢?

void myFunction(List<Node> nodes, List<Edge> edges) {   
    // 为代码简洁,省略了 collect(Collectors.toList())
    Type1 var1 = nodes.map(v -> getVar1(v));
    Type2 var2 = nodes.map(v -> getVar2(v));
    Type3 var3 = edges.map(v -> getVar3(v));
    Type4 var4 = edges.map(v -> getVar4(v));
    Type5 var5 = Stream.of(nodes, edges).filter(v -> filterVar5(v));
    
}

效果有大大的不同!

现在想追查哪个变量,简单轻松好多啦!

上次编辑于:
贡献者: levy