在 Java 流中,peek 真的只用于调试吗?
In Java streams is peek really only for debugging?
我正在阅读有关 Java 流的内容,并在阅读过程中发现新事物。我发现的新事物之一是 peek()
函数。我在 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();
})
您必须了解的重要一点是,流是由 终端操作 驱动的。终端操作确定是否必须处理所有元素或根本不处理任何元素。所以collect
是一个处理每个item的操作,而findAny
一旦遇到匹配的元素就可能停止处理item。
并且 count()
可以在不处理项目的情况下确定流的大小时根本不处理任何元素。由于这是一项未在 Java 8 中进行的优化,但会在 Java 9 中进行,因此当您切换到 Java 9 并让代码依赖于 count()
处理所有项目。这也与其他依赖于实现的细节有关,例如即使在 Java 9 中,参考实现也无法预测与 limit
结合的无限流源的大小,而没有阻止此类预测的基本限制。
因为 peek
允许“对每个元素执行提供的操作 当元素从结果流中消耗时 ”,它不强制处理元素但会执行操作取决于终端操作的需要。这意味着如果您需要特定的处理,例如,您必须非常小心地使用它。想要对所有元素应用一个动作。如果保证终端操作处理所有项目,它就可以工作,但即使那样,您也必须确保下一个开发人员不会更改终端操作(否则您会忘记那个微妙的方面)。
此外,虽然流保证即使对于并行流也能维持特定操作组合的相遇顺序,但这些保证不适用于 peek
。当收集到列表中时,生成的列表将具有有序并行流的正确顺序,但 peek
操作可能会以任意顺序并发调用。
因此,您可以使用 peek
做的最有用的事情是查明流元素是否已被处理,这正是 API 文档所说的:
This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline
从中得出的关键结论:
不要以无意的方式使用 API,即使它可以实现您的近期目标。 这种方法将来可能会失败,而且它也是未来的维护者不清楚。
将其分解为多个操作并没有什么坏处,因为它们是不同的操作。 以不明确和意外的方式使用 API 会造成危害,如果在 Java 的未来版本中修改此特定行为,可能会产生影响。
在此操作上使用 forEach
将使维护者清楚地知道 accounts
的每个元素都有 预期的 副作用,并且你正在执行一些可以改变它的操作。
从某种意义上说,它也更传统,因为 peek
是一个中间操作,在终端操作运行之前不会对整个集合进行操作,但 forEach
确实是一个终端操作。这样,您就可以围绕代码的行为和流程提出强有力的论据,而不是询问 peek
的行为是否与 forEach
在此上下文中的行为相同。
accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
也许经验法则应该是,如果您确实在 "debug" 场景之外使用 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;
而且您最终不会迭代一个集合两次。
虽然我同意上面的大多数答案,但在我的一个案例中,使用 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());
我会说 peek
提供了 分散代码的能力,这些代码可以改变流对象,或者修改全局状态 (基于它们),而不是塞满一切进入传递给终端方法的简单或组合函数。
现在的问题可能是:我们应该在函数式风格的函数中改变流对象还是改变全局状态java编程?
如果以上 2 个问题中的任何一个的答案是肯定的(或者:在某些情况下是肯定的)那么 peek()
就是 绝对不仅仅是为了调试目的 , 出于与 forEach()
不仅用于调试目的相同的原因。
对我来说,当在 forEach()
和 peek()
之间进行选择时,我选择了以下选项:我希望将改变流对象的代码片段附加到可组合项上,还是我希望它们附加到可组合项上?直接附加到流?
我认为 peek()
会更好地与 java9 方法配对。例如takeWhile()
可能需要决定何时根据已经发生变异的对象停止迭代,因此将其与 forEach()
配对不会产生相同的效果。
P.S. 我没有在任何地方提到 map()
因为万一我们想要改变对象(或全局状态),而不是生成新对象, 它的工作原理与 peek()
.
完全一样
功能解决方案是使帐户对象不可变。所以 account.login() 必须 return 一个新的帐户对象。这将意味着地图操作可用于登录而不是查看。
很多答案都提出了要点,尤其是 Makoto 的(接受的)答案非常详细地描述了可能出现的问题。但是没有人真正展示它是如何出错的:
[1]-> IntStream.range(1, 10).peek(System.out::println).count();
| ==> 9
无输出。
[2]-> IntStream.range(1, 10).filter(i -> i%2==0).peek(System.out::println).count();
| ==> 4
输出数字 2、4、6、8。
[3]-> IntStream.range(1, 10).filter(i -> i > 0).peek(System.out::println).count();
| ==> 9
输出数字 1 到 9。
[4]-> IntStream.range(1, 10).map(i -> i * 2).peek(System.out::println).count();
| ==> 9
无输出。
[5]-> Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).peek(System.out::println).count();
| ==> 9
无输出。
[6]-> Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream().peek(System.out::println).count();
| ==> 9
无输出。
[7]-> IntStream.range(1, 10).filter(i -> true).peek(System.out::println).count();
| ==> 9
输出数字 1 到 9。
[1]-> List<Integer> list = new ArrayList<>();
| list ==> []
[2]-> Stream.of(1, 5, 2, 7, 3, 9, 8, 4, 6).sorted().peek(list::add).count();
| ==> 9
[3]-> list
| list ==> []
(你懂的。)
示例是 jshell (Java 15.0.2) 中的 运行 并模仿转换数据的用例(例如将 System.out::println
替换为 list::add
在一些答案中完成)并返回添加了多少数据。目前的观察是,任何可以过滤元素的操作(例如过滤或跳过)似乎都强制处理所有剩余元素,但它不需要保持这种状态。
尽管 .peek
的文档说明说 “方法的存在主要是为了支持调试” 我认为它具有普遍意义。一方面,文档说“主要”,因此为其他用例留出了空间。它多年来一直没有被弃用,关于它被移除的猜测在 IMO 是徒劳的。
我想说,在我们仍然必须处理副作用方法的世界中,它有一个有效的位置和实用性。流中有许多使用副作用的有效操作。其他答案中已经提到了很多,我将在此处添加以在对象集合上设置标志,或在注册表中注册对象,然后在流中进一步处理这些对象。更不用说在流处理期间创建日志消息了。
我支持在单独的流操作中进行单独操作的想法,因此我避免将所有内容都推入最终 .forEach
。我更喜欢 .peek
而不是带有 lambda 的等效 .map
,除了调用副作用方法之外,唯一的目的是 return 传入的参数。 .peek
告诉我,一遇到这个操作,进去的也出来了,不用看lambda就知道了。从这个意义上说,它简洁、富有表现力并提高了代码的可读性。
话虽如此,我同意使用 .peek
时的所有注意事项,例如意识到它所使用的流的终端操作的影响。
我正在阅读有关 Java 流的内容,并在阅读过程中发现新事物。我发现的新事物之一是 peek()
函数。我在 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();
})
您必须了解的重要一点是,流是由 终端操作 驱动的。终端操作确定是否必须处理所有元素或根本不处理任何元素。所以collect
是一个处理每个item的操作,而findAny
一旦遇到匹配的元素就可能停止处理item。
并且 count()
可以在不处理项目的情况下确定流的大小时根本不处理任何元素。由于这是一项未在 Java 8 中进行的优化,但会在 Java 9 中进行,因此当您切换到 Java 9 并让代码依赖于 count()
处理所有项目。这也与其他依赖于实现的细节有关,例如即使在 Java 9 中,参考实现也无法预测与 limit
结合的无限流源的大小,而没有阻止此类预测的基本限制。
因为 peek
允许“对每个元素执行提供的操作 当元素从结果流中消耗时 ”,它不强制处理元素但会执行操作取决于终端操作的需要。这意味着如果您需要特定的处理,例如,您必须非常小心地使用它。想要对所有元素应用一个动作。如果保证终端操作处理所有项目,它就可以工作,但即使那样,您也必须确保下一个开发人员不会更改终端操作(否则您会忘记那个微妙的方面)。
此外,虽然流保证即使对于并行流也能维持特定操作组合的相遇顺序,但这些保证不适用于 peek
。当收集到列表中时,生成的列表将具有有序并行流的正确顺序,但 peek
操作可能会以任意顺序并发调用。
因此,您可以使用 peek
做的最有用的事情是查明流元素是否已被处理,这正是 API 文档所说的:
This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline
从中得出的关键结论:
不要以无意的方式使用 API,即使它可以实现您的近期目标。 这种方法将来可能会失败,而且它也是未来的维护者不清楚。
将其分解为多个操作并没有什么坏处,因为它们是不同的操作。 以不明确和意外的方式使用 API 会造成危害,如果在 Java 的未来版本中修改此特定行为,可能会产生影响。
在此操作上使用 forEach
将使维护者清楚地知道 accounts
的每个元素都有 预期的 副作用,并且你正在执行一些可以改变它的操作。
从某种意义上说,它也更传统,因为 peek
是一个中间操作,在终端操作运行之前不会对整个集合进行操作,但 forEach
确实是一个终端操作。这样,您就可以围绕代码的行为和流程提出强有力的论据,而不是询问 peek
的行为是否与 forEach
在此上下文中的行为相同。
accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
也许经验法则应该是,如果您确实在 "debug" 场景之外使用 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;
而且您最终不会迭代一个集合两次。
虽然我同意上面的大多数答案,但在我的一个案例中,使用 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());
我会说 peek
提供了 分散代码的能力,这些代码可以改变流对象,或者修改全局状态 (基于它们),而不是塞满一切进入传递给终端方法的简单或组合函数。
现在的问题可能是:我们应该在函数式风格的函数中改变流对象还是改变全局状态java编程?
如果以上 2 个问题中的任何一个的答案是肯定的(或者:在某些情况下是肯定的)那么 peek()
就是 绝对不仅仅是为了调试目的 , 出于与 forEach()
不仅用于调试目的相同的原因。
对我来说,当在 forEach()
和 peek()
之间进行选择时,我选择了以下选项:我希望将改变流对象的代码片段附加到可组合项上,还是我希望它们附加到可组合项上?直接附加到流?
我认为 peek()
会更好地与 java9 方法配对。例如takeWhile()
可能需要决定何时根据已经发生变异的对象停止迭代,因此将其与 forEach()
配对不会产生相同的效果。
P.S. 我没有在任何地方提到 map()
因为万一我们想要改变对象(或全局状态),而不是生成新对象, 它的工作原理与 peek()
.
功能解决方案是使帐户对象不可变。所以 account.login() 必须 return 一个新的帐户对象。这将意味着地图操作可用于登录而不是查看。
很多答案都提出了要点,尤其是 Makoto 的(接受的)答案非常详细地描述了可能出现的问题。但是没有人真正展示它是如何出错的:
[1]-> IntStream.range(1, 10).peek(System.out::println).count();
| ==> 9
无输出。
[2]-> IntStream.range(1, 10).filter(i -> i%2==0).peek(System.out::println).count();
| ==> 4
输出数字 2、4、6、8。
[3]-> IntStream.range(1, 10).filter(i -> i > 0).peek(System.out::println).count();
| ==> 9
输出数字 1 到 9。
[4]-> IntStream.range(1, 10).map(i -> i * 2).peek(System.out::println).count();
| ==> 9
无输出。
[5]-> Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).peek(System.out::println).count();
| ==> 9
无输出。
[6]-> Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream().peek(System.out::println).count();
| ==> 9
无输出。
[7]-> IntStream.range(1, 10).filter(i -> true).peek(System.out::println).count();
| ==> 9
输出数字 1 到 9。
[1]-> List<Integer> list = new ArrayList<>();
| list ==> []
[2]-> Stream.of(1, 5, 2, 7, 3, 9, 8, 4, 6).sorted().peek(list::add).count();
| ==> 9
[3]-> list
| list ==> []
(你懂的。)
示例是 jshell (Java 15.0.2) 中的 运行 并模仿转换数据的用例(例如将 System.out::println
替换为 list::add
在一些答案中完成)并返回添加了多少数据。目前的观察是,任何可以过滤元素的操作(例如过滤或跳过)似乎都强制处理所有剩余元素,但它不需要保持这种状态。
尽管 .peek
的文档说明说 “方法的存在主要是为了支持调试” 我认为它具有普遍意义。一方面,文档说“主要”,因此为其他用例留出了空间。它多年来一直没有被弃用,关于它被移除的猜测在 IMO 是徒劳的。
我想说,在我们仍然必须处理副作用方法的世界中,它有一个有效的位置和实用性。流中有许多使用副作用的有效操作。其他答案中已经提到了很多,我将在此处添加以在对象集合上设置标志,或在注册表中注册对象,然后在流中进一步处理这些对象。更不用说在流处理期间创建日志消息了。
我支持在单独的流操作中进行单独操作的想法,因此我避免将所有内容都推入最终 .forEach
。我更喜欢 .peek
而不是带有 lambda 的等效 .map
,除了调用副作用方法之外,唯一的目的是 return 传入的参数。 .peek
告诉我,一遇到这个操作,进去的也出来了,不用看lambda就知道了。从这个意义上说,它简洁、富有表现力并提高了代码的可读性。
话虽如此,我同意使用 .peek
时的所有注意事项,例如意识到它所使用的流的终端操作的影响。