Java流中peek的替代方法

3

Java Streams API 中有很多关于 peek 的问题。我正在寻找一种使用 Java Streams 完成以下常见模式的方法。我可以使用 Streams 让它工作,但是这并不明显,意味着没有注释会稍微危险,这并不理想。

boolean anyPricingComponentsChanged = false;
for (var pc : plan.getPricingComponents()) {
    if (pc.getValidTill() == null || pc.getValidTill().compareTo(dateNow) <= 0) {
        anyPricingComponentsChanged = true;
        pc.setValidTill(dateNow);
    }
}

我的选项:

long numberChanged = plan.getPricingComponents()
    .stream()
    .filter(pc -> pc.getValidTill() == null || pc.getValidTill().compareTo(dateNow) <= 0)
    .peek(pc -> pc.setValidTill(dateNow))
    .count(); //`count` rather than `findAny` to ensure that `peek` processes all components.

boolean anyPricingComponentsChanged = numberChanged != 0;

顺便提一下,虽然在这里compareTo不是一个昂贵的操作并且始终返回相同的结果,但在其他情况下可能并非如此,我宁愿避免为此模式运行多次。


1
我认为从根本上讲,流内部的任何有意的副作用都会面临同样的问题,即不明显且容易出错。我认为你提出的解决方案是合理的,但也需要注意其中的限制条件。我不认为有办法从根本上改变副作用在流中的非直观/意外性质。 - Joachim Sauer
1
并不是所有的东西都应该使用流来实现... - Didier L
@DidierL 是的,我认为这是正确的答案。对于我的简单情况来说,在代码行数或理解上并没有节省,这就是为什么我通常使用流的原因。 - mjaggard
1个回答

7
// 为确保 peek 处理所有组件 您无法确保 peek() 处理应修改的所有流元素。在某些情况下,此操作可以从管道中省略,并且您不应通过 peek() 执行任何重要操作。
以下是来自 peek() 文档 的引用:

API 注意:

此方法主要用于支持调试,您希望在流经管道中某个点时查看元素...

在流实现能够优化掉一些或所有元素的生成的情况下(例如使用 findFirst 等短路操作,或在 count() 中描述的示例中),对于那些元素,将不会调用该操作。

此外,这里是Stream API documentation关于副作用的说明:

如果行为参数确实具有副作用,除非明确声明,否则不能保证:

  • 那些副作用对其他线程的可见性;
  • 在同一个流管道中对“相同”元素进行不同操作的执行在相同的线程中;
  • 行为参数总是被调用,因为流实现可以省略(或整个阶段)从流管道中省略操作,如果它能够证明不会影响计算结果。

...

省略副作用也可能令人惊讶。除了终端操作forEachforEachOrdered之外,当流实现可以优化掉行为参数的执行而不影响计算结果时,行为参数的副作用可能并不总是被执行。 (有关特定示例,请参阅文档中记录的计数操作API注释。)

Amphesys添加

由于peek不应对流执行的结果产生影响,因此流实现可以自由地将其丢弃。

您可以使用以下方法而不是依赖于peek():

List<PricingComponent> componentsToChange = plan.getPricingComponents()
    .stream()
    .filter(pc -> pc.getValidTill() == null || pc.getValidTill().compareTo(dateNow) <= 0)
    .toList();
    
componentsToChange.forEach(pc -> pc.setValidTill(dateNow));

boolean anyPricingComponentsChanged = componentsToChange.size() != 0;

如果您不想将需要修改的对象实现为列表,则可以使用 for 循环。

注意

  • 来自 API文档 的上述引用,例如“如果流实现可以证明不会影响计算结果,则流管道中的任何中间操作都适用于省略操作(或整个阶段)”嵌入式副作用有关。如果副作用可以被省略,或者整个流水线阶段(流操作)对结果没有影响,则可以优化掉。为了在术语上达成一致,简而言之,副作用-是函数除了产生所需结果外所做的任何事情(例如:i -> { 副作用;返回i * 2; })。

  • 虽然将peek()分配给应在任何情况下执行的动作并不可取,但至少这个选择不会违反peek的语义。相反,通过filtermap或其他未设计通过副作用操作的操作进行副作用不仅无法解决问题,而且很奇怪,因为它违反了这些操作的语义并违反了最小惊奇原则


棘手的问题:在特定的测试用例中,将peek替换为map是否安全?在我看来,Javadoc未能阐明这一点。 - Andrey B. Panfilov
首先看起来很明显,“通常情况下”map不会影响count的结果,但是“特别说明”中提到peek操作存在问题。在我看来,实际上有问题的操作是count而不是peek。此外,我想知道如果任何操作都可能抛出运行时异常,如何证明之前的操作不会影响计算结果的实现方式。 - Andrey B. Panfilov
“有问题的操作实际上是 count” - count() 只是这种优化的一个例子。除了它之外,还有其他情况,其中 peek任何其他 中间操作 不会影响流执行结果,可以被优化掉。 - Alexander Ivanchenko
2
@AndreyB.Panfilov,最重要的是终端操作。终端操作告诉流你感兴趣的结果类型。在终端操作之外创建的任何其他“结果”都是不可靠的副作用。如果您的终端操作是.reduce(Integer::sum).orElse(0),则任何仍然产生正确总和的优化都是有效的。 - Holger
@AlexanderIvanchenko 我认为你的观点“坚持使用for循环”基本上是正确的答案。对于我这种简单的情况来说,在代码行数或理解上的简单性方面并没有节省,这就是为什么我通常使用Streams的原因。 - mjaggard
显示剩余2条评论

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