Stream map 与 peek
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Stream map 与 peek相关的知识,希望对你有一定的参考价值。
参考技术A 他们的相同之处在于,都是遍历流中的每个元素不通的地方在于,map接受的参数是一个带返回值的函数,map的执行结果是将参数中函数的执行结果;
而peek遍历流中每个元素,其遍历过程中对每个元素进行操作,但是其返回值仍然是流本身。
在 Java 流中,peek 真的只用于调试吗?
【中文标题】在 Java 流中,peek 真的只用于调试吗?【英文标题】:In Java streams is peek really only for debugging? 【发布时间】:2016-02-11 16:02:45 【问题描述】:我正在阅读有关 Java 流的信息,并在此过程中发现新事物。我发现的新事物之一是peek()
函数。我在 peek 上读到的几乎所有内容都表明它应该用于调试您的 Streams。
如果我有一个 Stream,其中每个 Account 都有一个用户名、密码字段和一个 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();
)
【问题讨论】:
每当我发现自己需要多行 lambda 时,我都会将这些行移至私有方法并传递方法引用而不是 lambda。 目的是什么 - 您是否尝试在中记录所有帐户并根据它们是否已登录来过滤它们(这可能是微不足道的)?或者,您是否要让他们登录,然后根据他们是否登录来过滤他们?我按这个顺序问这个问题是因为forEach
可能是您想要的操作,而不是peek
。仅仅因为它在 API 中并不意味着它不会被滥用(例如 Optional.of
)。
另请注意,您的代码可能只是.peek(Account::login)
和.filter(Account::loggedIn)
;没有理由编写只调用另一个类似方法的消费者和谓词。
还要注意行为参数中的流APIexplicitly discourages side-effects。
有用的消费者总是有副作用,当然不会气馁。这实际上在同一节中提到:“少量流操作,例如forEach()
和peek()
,只能通过副作用进行操作;这些应该小心使用。”。我的评论更多的是提醒peek
操作(它是为调试目的而设计的)不应该被在另一个操作中做同样的事情来代替,比如map()
或filter()
。
【参考方案1】:
您必须了解的重要一点是,流是由终端操作驱动的。终端操作确定是否必须处理所有元素或任何元素。所以collect
是一个处理每个项目的操作,而findAny
可能会在遇到匹配元素时停止处理项目。
当count()
可以在不处理项目的情况下确定流的大小时,它可能根本不处理任何元素。由于这不是在 Java 8 中进行的优化,而是将在 Java 9 中进行的优化,因此当您切换到 Java 9 并让代码依赖于 count()
处理所有项目时,可能会出现意外情况。这也与其他依赖于实现的细节有关,例如即使在 Java 9 中,参考实现也无法预测与 limit
结合的无限流源的大小,但没有基本限制阻止这种预测。
由于peek
允许“对每个元素执行提供的操作从结果流中消耗元素”,它不强制处理元素,但会根据终端执行操作操作需要。这意味着如果您需要特定的处理,例如,您必须非常小心地使用它。想要对所有元素应用操作。如果保证终端操作能够处理所有项目,它就可以工作,但即便如此,您也必须确保下一个开发人员不会更改终端操作(或者您忘记了那个微妙的方面)。
此外,虽然流保证即使对于并行流也能保持某种操作组合的相遇顺序,但这些保证不适用于peek
。当收集到一个列表中时,生成的列表将对有序并行流具有正确的顺序,但peek
操作可能会以任意顺序同时被调用。
因此,使用peek
可以做的最有用的事情是查明是否已处理流元素,这正是 API 文档所说的:
此方法的存在主要是为了支持调试,您希望在元素流过管道中的某个点时查看它们
【讨论】:
在 OP 的用例中,未来或现在会有什么问题吗?他的代码总是做他想做的事吗? @bayou.io:据我所知,这个确切的形式没有问题。但是正如我试图解释的那样,以这种方式使用它意味着您必须记住这一点,即使您在一两年后回到代码中将«feature request 9876»合并到代码中...... “peek 动作可能会以任意顺序同时被调用”。这是否违反了他们关于 peek 工作原理的规则,例如“随着元素被消耗”? @Jose Martinez:它说“因为元素被消耗从结果流中”,这不是终端动作而是处理,尽管即使是终端动作也可能消耗元素乱序只要最终结果一致。但我也认为,API 说明中的短语“当元素流过管道中的某个点时查看它们”在描述它方面做得更好。【参考方案2】:从中得出的关键结论:
不要以非预期的方式使用 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());
【讨论】:
如果您在预处理步骤中执行登录,则根本不需要流。您可以在源集合处执行forEach
:accounts.forEach(a -> a.login());
@Holger:好点。我已将其纳入答案。
@Adam.J:是的,我的回答更多地集中在您标题中包含的一般问题上,即通过解释该方法的各个方面,此方法是否真的仅用于调试。这个答案更符合您的实际用例以及如何去做。所以你可以说,它们一起提供了全貌。首先,这不是预期用途的原因,其次是结论,不要坚持非预期用途以及要做什么。后者对你会有更多的实际用途。
当然,如果login()
方法返回一个boolean
表示成功状态的值会容易得多……
这就是我的目标。如果login()
返回boolean
,您可以将其用作谓词,这是最干净的解决方案。它仍然有副作用,但只要不干扰就可以了,即一个Account
的login
进程对另一个Account
的登录进程没有影响。【参考方案3】:
也许经验法则应该是,如果您确实在“调试”场景之外使用 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;
而且您最终不会迭代集合两次。
【讨论】:
迭代两次是 O(2n) =~ O(n)。因此,您遇到性能问题的可能性很小。但是,如果你不使用 peek,你会在清晰度上得分。【参考方案4】:我想说peek
提供了分散可以改变流对象或修改全局状态的代码(基于它们)的能力,而不是把所有东西都塞进一个简单或组合函数传递给终端方法。
现在的问题可能是:我们应该在函数式 Java 编程中改变流对象还是从函数内部更改全局状态?
如果上述 2 个问题中的任何一个的答案是肯定的(或者:在某些情况下是肯定的),那么 peek()
绝对不仅仅是出于调试目的,出于同样的原因forEach()
不仅仅用于调试目的。
对我而言,在forEach()
和peek()
之间进行选择时,选择以下选项:我是否希望将变异流对象的代码片段附加到可组合对象,还是希望它们直接附加到流?
我认为peek()
会更好地与 java9 方法配对。例如takeWhile()
可能需要根据已经变异的对象决定何时停止迭代,因此将其与 forEach()
配对不会产生相同的效果。
PS我没有在任何地方提到map()
,因为如果我们想要改变对象(或全局状态),而不是生成新对象,它的工作原理与peek()
完全一样。
【讨论】:
【参考方案5】:虽然我同意上面的大多数答案,但我有一种情况,使用 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());
【讨论】:
您所要做的就是正确使用该登录方法。我真的不明白偷看是最干净的方式。阅读您的代码的人应该如何知道您实际上在滥用 API。好的和干净的代码不会强迫读者对代码做出假设。 我认为您需要在.peek
操作中限定方法引用,例如作为Account::login
,让它工作。
我同意使用.peek
代替.map
替代方案更简洁、富有表现力和易于理解。 .map 中的 lambda 只需要返回传入的对象。 .peek 自己做这件事。我一读到操作名称就知道了,不必检查 lambda 来找出它。【参考方案6】:
很多答案都提出了很好的观点,尤其是 Makoto 的(接受的)答案非常详细地描述了可能存在的问题。但实际上没有人表明它是如何出错的:
[1]-> IntStream.range(1, 10).peek(System.out::println).count();
| $6 ==> 9
没有输出。
[2]-> IntStream.range(1, 10).filter(i -> i%2==0).peek(System.out::println).count();
| $9 ==> 4
输出数字 2、4、6、8。
[3]-> IntStream.range(1, 10).filter(i -> i > 0).peek(System.out::println).count();
| $12 ==> 9
输出数字 1 到 9。
[4]-> IntStream.range(1, 10).map(i -> i * 2).peek(System.out::println).count();
| $16 ==> 9
没有输出。
[5]-> Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).peek(System.out::println).count();
| $23 ==> 9
没有输出。
[6]-> Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream().peek(System.out::println).count();
| $25 ==> 9
没有输出。
[7]-> IntStream.range(1, 10).filter(i -> true).peek(System.out::println).count();
| $30 ==> 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();
| $7 ==> 9
[3]-> list
| list ==> []
(你明白了。)
这些示例在 jshell (Java 15.0.2) 中运行,并模拟了转换数据的用例(例如,将 System.out::println
替换为 list::add
,正如在某些答案中所做的那样)并返回添加了多少数据。目前的观察是,任何可以过滤元素的操作(例如过滤或跳过)似乎都会强制处理所有剩余的元素,但它不必保持这种状态。
【讨论】:
我不确定您的结果是否可靠。由于.count
终端操作也会产生输出,JShell 可能会用它覆盖 .peek 操作的输出。如果将 .count 替换为另一个不产生输出的终端操作,它会很好地工作,例如jshell> IntStream.range(1,10).peek(System.out::println).forEach(i->)
.
这里算终端操作正是我想展示的问题。 count 对您的实际元素不感兴趣,这就是为什么有时不处理它们而只计算计数的原因。
啊,好的,现在我明白了。
对于任何想知道count()
方法如何在不实际计算流中元素数量的情况下工作的人,我坚信这是因为IntStream
和@987654336 设置了标志StreamOpFlag.SIZED
@。更糟糕的是,Stream.of
的行为在 JVM 版本之间有所不同:在 1.8 中它曾经是一个普通流,但在后来的某些版本中它变成了 SIZED
,iirc。【参考方案7】:
尽管.peek
的文档注释说“方法的存在主要是为了支持调试”我认为它具有普遍的相关性。一方面,文档说“主要”,因此为其他用例留出了空间。多年来它一直没有被弃用,关于它被移除的猜测是徒劳的。
我想说,在我们仍然必须处理副作用方法的世界中,它具有有效的位置和实用性。流中有许多使用副作用的有效操作。在其他答案中已经提到了许多,我将在此处添加以在对象集合上设置标志,或将它们注册到注册表,然后在流中进一步处理的对象上。更不用说在流处理期间创建日志消息了。
我支持在单独的流操作中具有单独的操作的想法,因此我避免将所有内容都推入最终的.forEach
。我更喜欢 .peek
而不是等效的 .map
带有一个 lambda,除了调用副作用方法之外,它的唯一目的是返回传入的参数。 .peek
告诉我,只要遇到这个操作,进来的东西也会出去,我不需要阅读 lambda 来找出。从这个意义上说,它简洁、富有表现力并提高了代码的可读性。
话虽如此,我同意使用.peek
时的所有注意事项,例如意识到使用它的流的终端操作的影响。
【讨论】:
【参考方案8】:功能解决方案是使帐户对象不可变。所以 account.login() 必须返回一个新的帐户对象。这意味着地图操作可以用于登录而不是窥视。
【讨论】:
以上是关于Stream map 与 peek的主要内容,如果未能解决你的问题,请参考以下文章
jdk8 stream可以与list,map等数据结构互相转换