在Java流中,偷看真的只是为了调试吗?

2022-08-31 06:26:47

我正在阅读有关Java流的信息,并在前进的过程中发现新事物。我发现的一个新东西是功能。我在peek上读到的几乎所有内容都说它应该用于调试您的Streams。peek()

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

我也有

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

答案 1

您必须了解的重要一点是,流是由终端操作驱动的。终端操作确定是否必须处理所有元素或根本必须处理任何元素。处理每个项目的操作也是如此,而一旦遇到匹配的元素,可能会停止处理项目。collectfindAny

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

由于peek允许“在从生成的流中消耗元素时对每个元素执行提供的操作”,因此它不强制处理元素,而是根据终端操作的需求执行操作。这意味着如果您需要特定的处理,例如,想要对所有元素应用操作,则必须非常小心地使用它。如果终端操作保证处理所有项目,它就可以工作,但即使这样,您也必须确保不是下一个开发人员更改终端操作(或者您忘记了那个微妙的方面)。

此外,虽然流保证保持某种操作组合的遭遇顺序,即使对于并行流也是如此,但这些保证不适用于 。收集到列表中时,生成的列表将具有有序并行流的正确顺序,但该操作可能会以任意顺序并发调用。peekpeek

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

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


答案 2

关键点:

不要以意想不到的方式使用 API,即使它实现了你的直接目标。这种方法可能会在未来中断,未来的维护者也不清楚。


将其分解为多个操作没有坏处,因为它们是不同的操作。以不明确和意想不到的方式使用API有害的,如果在将来的Java版本中修改此特定行为,则可能会产生影响。

在此操作上使用将向维护者清楚地表明,对 的每个元素都有预期的副作用,并且您正在执行一些可以改变它的操作。forEachaccounts

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

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

推荐