我正在阅读有关Java流的资料,并在阅读过程中发现新的东西。我发现的一个新东西是peek()函数。几乎所有我读到的peek说它应该用来调试你的流。

如果我有一个流,其中每个帐户都有一个用户名,密码字段和login()和loggedIn()方法。

我还有

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

and

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

当前回答

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

与您的用例类似,假设您希望仅对活动帐户进行过滤,然后在这些帐户上执行登录。

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

其他回答

您必须了解的重要一点是,流是由终端操作驱动的。终端操作决定是否必须处理所有元素,或者是否必须处理任何元素。因此collect是处理每个项的操作,而findAny在遇到匹配元素时可能会停止处理项。

And count() may not process any elements at all when it can determine the size of the stream without processing the items. Since this is an optimization not made in Java 8, but which will be in Java 9, there might be surprises when you switch to Java 9 and have code relying on count() processing all items. This is also connected to other implementation-dependent details, e.g. even in Java 9, the reference implementation will not be able to predict the size of an infinite stream source combined with limit while there is no fundamental limitation preventing such prediction.

Since peek allows “performing the provided action on each element as elements are consumed from the resulting stream”, it does not mandate processing of elements but will perform the action depending on what the terminal operation needs. This implies that you have to use it with great care if you need a particular processing, e.g. want to apply an action on all elements. It works if the terminal operation is guaranteed to process all items, but even then, you must be sure that not the next developer changes the terminal operation (or you forget that subtle aspect).

此外,虽然流保证维持某些操作组合的遇到顺序,即使是并行流,但这些保证并不适用于peek。当收集到列表中时,结果列表将具有有序并行流的正确顺序,但peek操作可能以任意顺序并发调用。

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

此方法的存在主要是为了支持调试,在调试中,您希望看到元素流经管道中的某个点时的情况

尽管.peek的文档说明说“方法的存在主要是为了支持调试”,但我认为它具有普遍的相关性。首先,文档说“主要”,所以为其他用例留下了空间。多年来它没有被弃用,在我看来,关于它被移除的猜测是徒劳的。

我想说,在一个我们仍然需要处理副作用方法的世界里,它有一个有效的位置和效用。流中有许多使用副作用的有效操作。许多已经在其他答案中提到,我只是在这里添加一个对象集合上设置一个标志,或者将它们注册到注册表中,然后在流中进一步处理对象。更不用说在流处理期间创建日志消息了。

我支持在不同的流操作中有不同的动作的想法,因此我避免将所有内容都推入最终的. foreach。我更喜欢.peek而不是等效的带有lambda的.map,除了调用副作用方法之外,它的唯一目的是返回传入的in参数。.peek告诉我,一旦遇到这个操作,输入的内容也会输出,而且我不需要读取lambda来找出。从这个意义上说,它是简洁的,富有表现力的,并提高了代码的可读性。

话虽如此,我同意使用.peek时的所有注意事项,例如,注意使用.peek的流的终端操作的影响。

为了消除警告,我使用了函数子tee,以Unix的tee命名:

public static <T> Function<T,T> tee(Consumer<T> after) {
    return arg -> {
        f.accept(arg);
        return arg;
    };
}

你可以替换:

  .peek(f)

with

  .map(tee(f))

关键是:

不要以一种意想不到的方式使用API,即使它实现了您的直接目标。这种方法在将来可能会失效,而且对于未来的维护者来说也不清楚。


将其分解为多个操作并没有害处,因为它们是不同的操作。以一种不明确和意外的方式使用API是有害的,如果在未来的Java版本中修改了这种特定的行为,可能会产生后果。

在此操作中使用forEach将使维护者清楚地知道帐户的每个元素都有预期的副作用,并且您正在执行一些可能会改变它的操作。

它也更传统,因为peek是一个中间操作,在终端操作运行之前不会对整个集合进行操作,但forEach确实是一个终端操作。通过这种方式,您可以围绕代码的行为和流程进行强有力的论证,而不是询问peek在此上下文中的行为是否与forEach相同。

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

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

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

似乎是一个有效的情况,在一个操作中,将所有的foo转换为bar,并告诉他们都是你好。

似乎比下面的东西更高效和优雅:

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

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