在流管道中保存到数据库

Tit*_*lum 7 java functional-programming side-effects java-8 java-stream

根据Oracle 网站上的文档

通常不鼓励流操作的行为参数的副作用,因为它们通常会导致无意中违反无状态要求,以及其他线程安全危险。

这是否包括将流的元素保存到数据库?

想象一下以下(伪)代码:

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  return cars.stream()
           .map(this::saveCar)
           .collect(Collectors.toList());
}
Run Code Online (Sandbox Code Playgroud)

与此实现相反的不良影响是什么:

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  List<SavedCars> savedCars = new ArrayList<>();
  for (Cat car : cars) {
    savedCars.add(this.saveCar(car));
  }
  return savedCars.
}
Run Code Online (Sandbox Code Playgroud)

Eug*_*ene 6

最简单的例子是:

cars.stream()
    .map(this:saveCar)
    .count()
Run Code Online (Sandbox Code Playgroud)

在这种情况下,从 java-9 开始,map将不会被执行;因为您根本不需要它来知道count, 。

在其他多种情况下,副作用会给您带来很多痛苦;在一定条件下。

  • @Titulum,这是一个_完全_不同的问题,取决于实现;但是,是的,这样的事情应该在终端操作中实现,就像自定义的“Collector”。 (2认同)
  • @Titulum 这不会在任何地方被记录下来。这些是实施细节;但如果你遵循文档(比如你的副作用部分) - 你就不会关心它们,不是吗? (2认同)
  • @Titulum Java 8 功能并没有将 Java 变成一种函数式语言,而 Streams 等并不是“替代品”,它们只是一个附加工具。将内容保存到数据库中是一个“巨大”的副作用,因此您试图硬塞一些不适合的东西,只是因为您喜欢函数式编程的想法。如果您想实现所有功能,您可能需要查看 [Clojure](https://clojure.org)。 (2认同)

wal*_*len 4

\n

根据 Oracle 网站上的文档 [...]

\n
\n\n

该链接适用于 Java 8。您可能需要阅读 Java 9(2017 年发布)及更高版本的文档,因为它们在这方面更加明确。具体来说:

\n\n
\n

流实现在优化结果计算方面允许很大的自由度。例如,如果流实现能够证明它不会影响计算结果,那么它可以自由地从流管道中省略操作(或整个阶段),从而省略行为参数的调用。这意味着行为参数的副作用可能并不总是被执行,并且不应依赖,除非另有指定(例如通过终端操作forEachforEachOrdered)。(有关此类优化的具体示例,请参阅有关该操作的 API 说明count()。有关更多详细信息,请参阅流包文档的 副作用部分。)

\n\n

来源:Java 9 接口的StreamJavadoc

\n
\n\n

还有您引用的文档的更新版本:

\n\n
\n

副作用

\n\n

一般来说,不鼓励流操作的行为参数产生副作用,因为它们通常会导致无意中违反无状态性要求,以及其他线程安全隐患。
\n 如果行为参数确实有副作用,除非明确说明,否则不保证

\n\n
    \n
  • 这些副作用对其他线程的可见性;
  • \n
  • 同一流管道中“同一”元素上的不同操作在同一线程中执行;和
  • \n
  • 行为参数总是被调用,因为如果流实现可以证明它不会影响计算结果,那么它可以自由地从流管道中删除操作(或整个阶段)。
  • \n
\n\n

副作用的顺序可能会令人惊讶。即使管道被限制为产生与流源的遭遇顺序一致的结果(例如,IntStream.range(0,5).parallel().map(x -> x*2).toArray()必须产生[0, 2, 4, 6, 8]),也不能保证映射器函数应用于各个元素的顺序,或者在对于给定元素执行什么线程任何行为参数。

\n\n

副作用的消失也可能令人惊讶。除了终端操作forEachforEachOrdered之外,当流实现可以优化行为参数的执行而不影响计算结果时,行为参数的副作用可能并不总是被执行。(有关具体示例,请参阅有关该操作的 API 注释count。)

\n\n

来源:Java 9 包的java.util.streamJavadoc

\n
\n\n

全部都是我的重点。

\n\n

如您所见,当前的官方文档更详细地介绍了如果您决定在流操作中使用副作用时可能遇到的问题。它也非常清楚forEach,并且forEachOrdered是唯一保证执行副作用的终端操作(请注意,线程安全问题仍然适用,如官方示例所示)。

\n\n
\n\n

话虽如此,关于您的具体代码,仅表示代码:

\n\n
public List<SavedCars> saveCars(List<Car> cars) {\n  return cars.stream()\n           .map(this::saveCar)\n           .collect(Collectors.toList());\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我发现上述代码没有与 Streams 相关的问题。

\n\n
    \n
  • .map()步骤将被执行,因为.collect()可变的归约操作,这是官方文档推荐的而不是类似的操作.forEach(list::add))依赖于.map()\ 的输出,并且由于此(即saveCar()\ 的)输出与其输入不同,因此流不能“证明[省略]它不会影响计算结果”
  • \n
  • 它不是一个parallelStream(),所以它不应该引入任何以前不存在的并发问题(当然,如果有人.parallel()稍后添加一个,那么可能会出现问题\xe2\x80\x94,就像有人决定并行化一个for通过启动新的并行循环一样用于内部计算的线程)。
  • \n
\n\n

这并不意味着该示例中的代码是 Good Code\xe2\x84\xa2。.stream.map(::someSideEffect()).collect()作为对集合中的每个项目执行副作用操作的方式的序列可能看起来更简单/简短/优雅?比其for对应物更重要,有时可能是这样。然而,正如尤金、霍尔格和其他一些人告诉你的那样,有更好的方法来解决这个问题。
\n快速思考一下:启动 aStream与迭代简单的成本for不可忽略,除非您有很多项目,并且如果您有很多项目,那么您:a)可能不想制作新的每个数据库都可以访问,所以有一个saveAll(List items)API 会更好;b) 可能不想承受大量处理带来的性能损失的性能损失,因此您最终会使用并行化,然后出现一系列全新的问题。

\n