在Javastream中偷窥真的只用于debugging?

我正在阅读关于Javastream和发现新的东西,因为我走。 我发现的新东西之一是peek()函数。 我几乎读过的所有东西都说它应该用来debugging你的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()); 

现在据我所知,这正是它打算做的。 它;

  • 提供一个帐户清单
  • 尝试login到每个帐户
  • 过滤掉任何未login的帐户
  • 将login帐户收集到新列表中

做这样的事情有什么缺点? 任何理由我不应该继续? 最后,如果不是这个解决scheme那么是什么?

这个原始版本使用.filter()方法如下;

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

从这个关键的东西:

不要以非预期的方式使用API​​,即使它实现了您的直接目标。 这种方法将来可能会打破,而且未来的维护者也不清楚。


将其分解为多个操作没有什么坏处,因为它们是不同的操作。 以不明确和不明确的方式使用API 造成危害,如果在未来的Java版本中修改此特定行为,可能会产生分歧。

在这个操作中使用forEach会让维护人员清楚每个accounts元素都有一个预期的副作用,并且您正在执行一些可以改变它的操作。

在这个意义上说, peek是一个中间操作,在terminal操作运行之前,它不会在整个集合上运行,而每个操作确实是一个terminal操作。 通过这种方式,您可以围绕代码的行为和stream程进行强有力的争论,而不是询问有关peek行为与forEach在此情况下是否相同的问题。

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

你必须明白的重要的事情是docker操作驱动码stream。 terminal操作确定是否所有的元素都必须被处理,或者根本不需要处理。 所以collect是一个处理每个项目的操作,而findAny可能会在遇到匹配元素时停止处理项目。

并且count()可能不处理任何元素,因为它可以在不处理项目的情况下确定stream的大小。 由于这是一个不是在Java 8中进行的优化,而是在Java 9中进行的优化,当您切换到Java 9并且依靠count()处理所有项目时,可能会出现意外。 这也与其他实现相关的细节相关联,例如,即使在Java 9中,参考实现也将无法预测无限stream源与limit相结合的大小,而没有阻止这种预测的基本限制。

由于peek允许“在元素从结果stream中消耗时对每个元素执行提供的操作”,因此它不会强制处理元素,而是会根据terminal操作的需要执行操作。 这意味着如果您需要特定的处理,您必须非常小心地使用它,例如,要对所有元素应用操作。 如果terminal操作保证处理所有项目,那么它是有效的,但是即使如此,你也必须确保下一个开发者不会改变terminal操作(或者你忘记了这个微妙的方面)。

此外,尽pipestream保证即使对于并行stream仍保持对某些操作组合的遇到命令,但这些保证不适用于peek 。 当收集到列表中时,结果列表将具有对于有序并行stream的正确顺序,但是可以以任意顺序并发地调用peek操作。

所以你可以用peek来做的最有用的事情是找出一个stream元素是否已经被处理,这正是API文档所说的:

这种方法主要是为了支持debugging,在stream水线中的特定点上stream过的元素

也许一个经验法则应该是,如果你在“debugging”场景之外使用偷看,你应该这样做,如果你确定终止和中间过滤条件是什么。 例如:

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

似乎是一个有效的情况下,你想在一个操作中转换所有Foos酒吧,并告诉他们所有你好。

似乎比以下更有效率和优雅:

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

而且你不会最终迭代一个集合两次。

虽然我同意上面的大多数答案,但是我有一个使用peek实际上似乎是最干净的方式的情况。

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

 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());