在Java流中,peek真的仅用于调试吗?


137

我正在阅读有关Java流的信息,并在不断学习中发现新事物。我发现的新peek()功能之一就是功能。我偷看的几乎所有内容都说应将其用于调试Streams。

如果我有一个Stream,其中每个帐户都有一个用户名,密码字段以及一个login()和LoggedIn()方法,该怎么办?

我也有

Consumer<Account> login = account -> account.login();

Predicate<Account> loggedIn = account -> account.loggedIn();

为什么会这么糟糕?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

现在,据我所知,这确实可以实现预期的目的。它;

  • 取得帐户清单
  • 尝试登录每个帐户
  • 过滤掉所有未登录的帐户
  • 将已登录的帐户收集到新列表中

这样做的缺点是什么?有什么我不应该继续的理由吗?最后,如果不是这种解决方案,那又如何呢?

它的原始版本使用.filter()方法,如下所示;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

38
每当我发现自己需要多行lambda时,就将这些行移至私有方法并传递方法引用而不是lambda。
VGR

1
目的是什么-您是否尝试登录所有帐户根据它们是否已登录来过滤它们(可能确实如此)?或者,您是否要登录它们,然后根据它们是否已登录对其进行过滤?我按此顺序询问,因为这forEach可能是您要执行的操作peek。仅仅因为它包含在API中并不意味着它不会被滥用(例如Optional.of)。
Makoto 2015年

8
还要注意,您的代码可以是.peek(Account::login)and .filter(Account::loggedIn); 没有理由写一个Consumer和Predicate只调用另一个这样的方法。
约书亚·泰勒

2
另请注意,流API 明确阻止行为参数中的副作用
Didier L

6
有用的消费者总是会产生副作用,当然,我们不会灰心。实际上在同一部分中提到了这一点:“ 少数流操作,例如forEach()peek(),只能通过副作用进行操作;这些应谨慎使用。”。我要特别提醒的是,该peek操作(设计用于调试目的)不应由在另一操作(例如map()或)内执行相同操作来代替filter()
Didier L

Answers:


77

关键要点是:

即使达到您的近期目标,也不要以意外的方式使用该API。这种方法将来可能会中断,而且对于将来的维护者来说也不清楚。


将其分解为多个操作是没有害处的,因为它们是不同的操作。还有就是在不明确的和意想不到的方式,如果这种特定的行为是用Java的未来版本中修改其可能的后果使用API的伤害。

使用forEach该操作将明确维护者,有一个预期中的每个元素的副作用accounts,并且正在执行某些操作,可能发生变异它。

从某种意义上说,它peek是一种更常规的中间操作,它在终端操作运行之前不会对整个集合进行操作,而forEach实际上是终端操作。这样,您可以围绕代码的行为和流程提出强有力的论据,而不是询问是否peek行为与forEach在此情况下相同。

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

3
如果您在预处理步骤中执行登录,则根本不需要流。您可以forEach直接在来源集合中表演:accounts.forEach(a -> a.login());
Holger 2015年

1
@Holger:很好。我已经将其纳入答案。
Makoto 2015年

2
@ Adam.J:是的,我的回答更多地集中在标题中包含的一般问题上,即,通过解释该方法的各个方面,该方法是否真的仅用于调试。这个答案更多地融合在您的实际用例以及如何使用上。因此,您可以说,它们一起提供了完整的画面。首先,这不是预期用途的原因,其次是结论,不要坚持非预期用途以及该怎么做。后者将为您提供更多实际用途。
Holger

2
当然,如果该login()方法返回一个boolean指示成功状态的值,则容易得多……
Holger 2015年

3
这就是我的目标。如果login()返回a boolean,则可以将其用作最干净的谓词。它仍然有副作用,但是可以,只要它不干扰,即login一个Account进程对另一个进程的登录过程没有影响Account
Holger 2015年

111

您必须了解的重要一点是,流是由终端操作驱动的。终端操作确定是否必须处理所有元素还是根本要处理任何元素。所以collect是处理的每个项目,而操作findAny可能停止处理的物品,一旦遇到的匹配元件。

并且count()可以在不处理项目的情况下确定流的大小时根本不处理任何元素。由于这不是Java 8的优化,而是Java 9的优化,因此当您切换到Java 9并使代码依赖于count()处理所有项目时,可能会感到惊讶。这也与其他依赖于实现的细节相关联,例如即使在Java 9中,参考实现也将无法预测无限流源的组合大小,limit而没有任何基本限制可以阻止这种预测。

由于peek允许“ 随着元素从结果流中消耗而在每个元素上执行所提供的操作”,因此它不要求对元素进行处理,而是根据终端操作的需要执行操作。这意味着,如果需要特定的处理,例如要对所有元素应用操作,则必须格外小心。如果保证终端操作可以处理所有项目,则该方法有效,但是即使如此,您仍必须确保下一位开发人员不会更改终端操作(否则您会忘记该微妙的方面)。

此外,尽管流保证即使对于并行流,也可以维持操作的某些组合的遇到顺序,但这些保证不适用于peek。当收集到一个列表中时,生成的列表将对有序并行流具有正确的顺序,但是peek可以按任意顺序并发地调用该操作。

因此,您可以做的最有用的事情peek是找出流元素是否已被处理,这正是API文档所说的:

存在此方法主要是为了支持调试,您需要在其中查看流过管道中特定点的元素


OP的用例中是否存在任何问题?他的代码是否总是按他的意愿去做?
羽2015年

9
@ bayou.io:据我所知,这种确切形式没有问题。但是,正如我试图解释的那样,以这种方式使用它意味着您必须记住这一方面,即使您在一年或两年后回到将“功能请求9876”并入代码中的代码…
Holger

1
“窥视动作可以以任意顺序并发地被调用”。这句话是否违反了他们对偷看工作的规则,例如“消耗元素”?
Jose Martinez

5
@Jose Martinez:它说“因为元素从结果流中消耗”,这不是终端操作,而是处理,即使终端操作只要最终结果一致也可以无序使用元素。但是我也认为,API注释的短语“ 在流经管道中的某个点时查看元素 ”在描述它方面做得更好。
Holger

23

经验法则可能是,如果您确实在“调试”方案之外使用peek,则只有在确定终止过滤条件和中间过滤条件是什么时,才应该这样做。例如:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

似乎很可能是您想要的有效案例,只需执行一次操作即可将所有Foos转换为Bars,并告诉他们一切。

似乎比以下方式更高效,更优雅:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

而且您最终不会对集合进行两次迭代。


4

我要说的是,peek它可以分散代码以使流对象发生变化或修改全局状态(基于它们),而不必将所有内容填充到传递给终端方法的简单或组合函数中

现在的问题可能是:我们应该在函数风格的Java编程中从函数内部更改流对象还是更改全局状态

如果回答任何的上述2个问题是肯定的(或:在某些情况下是),则peek()绝对不仅是为了调试的目的对于同样的原因,forEach()不仅是为了调试的目的

对我而言,在forEach()和之间peek()进行选择时,要选择以下内容:我是否希望将使流对象变异的代码片段附加到可组合对象,还是希望它们直接附加到流?

我认为peek()最好与java9方法配对。例如,takeWhile()可能需要基于已突变的对象来决定何时停止迭代,因此对其进行解析forEach()将不会产生相同的效果。

PS我没有map()在任何地方引用过,因为万一我们想改变对象(或全局状态),而不是生成新对象,它的工作原理就像peek()


3

尽管我同意上面的大多数答案,但我有一种情况,实际上使用偷看似乎是最干净的方法。

与您的用例类似,假设您只想对活动帐户进行过滤,然后对这些帐户执行登录。

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek有助于避免不必要的调用,而不必重复两次迭代集合:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

3
您所要做的就是正确设置该登录方法。我真的不知道偷看是最干净的方法。读取您的代码的人应该如何得知您实际上滥用了该API。好的和干净的代码不会强迫读者对代码进行假设。
kaba713

1

功能解决方案是使帐户对象不可变。因此account.login()必须返回一个新的帐户对象。这意味着映射操作可以用于登录而不是窥视。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.