Java 8中Lambda表达式的副作用

5
请看下面的代码片段。
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
List<String> copyList = new ArrayList<String>(); 
Consumer<String> consumer = s->copyList.add(s);
list.stream().forEach(consumer);

由于我们正在使用lambda表达式,根据函数式编程(纯函数)的规定,它应该仅计算输入并提供相应的输出。

但在此示例中,它尝试将元素添加到列表中,而该列表既不是输入也不是在lambda作用域内声明的。

这是一种好的做法吗?会导致任何副作用吗?


最后几行可以重构为 list.stream().forEach(copyList::add); - Glains
forEach从来都不是纯函数,因为如果没有副作用,它就无法对流执行任何操作。这适用于Scheme中的for-each,JS中的Array.forEach等。 - Sylwester
2个回答

10
如果forEach没有副作用,那么它就毫无用处,因为它没有返回值。因此,每当你使用forEach时,都应该期望发生副作用。因此,你的示例没有任何问题。
一个Consumer<String>可以打印String,或将其插入到某个数据库中,或将其写入某个输出文件中,或将其存储在某个Collection中(如您的示例中),等等...
Stream Javadoc中得知:
流管道由源(可以是数组、集合、生成器函数、I/O通道等)、零个或多个中间操作(将流转换为另一个流,例如Stream.filter(Predicate))和终端操作(产生结果或副作用,例如Stream.count()或Stream.forEach(Consumer))组成。
此外,如果查看Consumer的Javadoc,可以看到它被期望有副作用:
java.util.function.Consumer 表示接受单个输入参数并且没有返回结果的操作。与大多数其他功能接口不同,Consumer预计通过副作用进行操作。

我想这意味着Java的Stream和函数式接口并不仅仅是为了“纯”函数式编程而设计的。


3
通常情况下,就定义而言,"Consumer"旨在产生副作用。 - shmosel

2
对于forEach甚至stream().forEach,它非常简单。 您的示例运行良好。 但请注意,如果您使用其他流方法,则可能会遇到一些意外情况:例如,以下代码不会打印任何内容。
List<String> lst = Arrays.asList("a", "b", "c");

lst.stream().map(
 s -> {
   System.out.println(s);
   return "-";
 });

在这种情况下,流的作用更像是一个构建器,它准备了一个过程但尚未执行。只有在调用collectcountfind...方法时,Lambda表达式才会被执行。通过查看map方法的返回类型,可以轻松地发现这一点,而该返回类型本身又是一个Stream
话虽如此,针对您的特定示例,我认为有更简单的替代方案。
List<String> list = Arrays.asList("A", "B", "C");

// this is the base pattern for transforming one list to another.
// the map applies a transformation.
List<String> copyList1 = list.stream().map(e -> e).collect(Collectors.toList());

// if it's a 1-to-1 mapping, then you don't really need the map.
List<String> copyList2 = list.stream().collect(Collectors.toList());

// in essence, you could of course just clone the list without streaming.
List<String> copyList3 = new ArrayList<>(list);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接