在 Java 流中,peek 真的只用于调试吗?

     2023-02-15     45

关键词:

【中文标题】在 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());

【讨论】:

如果您在预处理步骤中执行登录,则根本不需要流。您可以在源集合处执行forEachaccounts.forEach(a -&gt; a.login()); @Holger:好点。我已将其纳入答案。 @Adam.J:是的,我的回答更多地集中在您标题中包含的一般问题上,即通过解释该方法的各个方面,此方法是否真的仅用于调试。这个答案更符合您的实际用例以及如何去做。所以你可以说,它们一起提供了全貌。首先,这不是预期用途的原因,其次是结论,不要坚持非预期用途以及要做什么。后者对你会有更多的实际用途。 当然,如果login() 方法返回一个boolean 表示成功状态的值会容易得多…… 这就是我的目标。如果login() 返回boolean,您可以将其用作谓词,这是最干净的解决方案。它仍然有副作用,但只要不干扰就可以了,即一个Accountlogin进程对另一个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&gt; IntStream.range(1,10).peek(System.out::println).forEach(i-&gt;). 这里算终端操作正是我想展示的问题。 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() 必须返回一个新的帐户对象。这意味着地图操作可以用于登录而不是窥视。

【讨论】:

我们真的需要在 OIDC 的隐式流中使用 id_token 吗?

】我们真的需要在OIDC的隐式流中使用id_token吗?【英文标题】:Dowereallyneedid_tokeninimplicitflowinOIDC?【发布时间】:2018-07-2319:02:28【问题描述】:我正在开发一个SPA应用程序,我正在使用推荐的隐式流程,并且能够获取access_token和id... 查看详情

关于javastream流中的peek方法和foreach的自我理解:(代码片段)

场景:我需要在数据库中将信息查出,在将其转化为某一个对象,再将该对象进行添加进入list集合 我首先想到的是stream().peek()方法,编写的代码如下:productAttrValueService.getListBySpuIdAndAttrId(spuInfo,attrAttrgroupRelationEntity.getAttr... 查看详情

PHP有peek数组操作吗?

...list);array_unshift($list,$item);return$item;这段代码对我来说似乎真的很重,并且peek通常由队列和堆 查看详情

streammap与peek

参考技术A他们的相同之处在于,都是遍历流中的每个元素不通的地方在于,map接受的参数是一个带返回值的函数,map的执行结果是将参数中函数的执行结果;而peek遍历流中每个元素,其遍历过程中对每个元素进行操作,但是其... 查看详情

真的有必要在 JWT 中内置有效负载吗?

】真的有必要在JWT中内置有效负载吗?【英文标题】:IsreallynecessarytohaveapayloadbuiltintoJWT?【发布时间】:2020-06-2717:32:25【问题描述】:每个描述JWT用法的示例代码都在谈论一个有效负载,通常是用户信息,例如名称和角色。我想... 查看详情

来自流中的字符串,用于多种对象类型

】来自流中的字符串,用于多种对象类型【英文标题】:stringfromstreamingoformultipleobjecttypes【发布时间】:2014-12-1716:54:22【问题描述】:我习惯了Java,并在googlego中设置了第一步。我有一棵带有子对象等的对象树……这棵树递归地... 查看详情

c++中的peek()函数及其用法(代码片段)

...指针仍停留在当前位置,并不后移。其功能是从输入流中读取一个字符,但该字符并未从输入流中删除。 若把输入流比作一 查看详情

在发布模式构建中可调试是真的吗?

】在发布模式构建中可调试是真的吗?【英文标题】:debuggableistrueinreleasemodebuild?【发布时间】:2016-11-3010:31:04【问题描述】:debuggable=true处于发布模式apk。当我尝试对这个属性进行硬编码时,我遇到了错误"避免对调试模式进行... 查看详情

如何在 C# 中从内存流中播放 MP3?

】如何在C#中从内存流中播放MP3?【英文标题】:HowcanIplayanMP3fromamemorystreaminC#?【发布时间】:2010-12-2211:35:25【问题描述】:我正在尝试播放MP3声音,但我需要从内存流中播放它(我没有实际文件)。我最好的选择是什么?mciSendS... 查看详情

SELECT * 真的比只选择需要的列花费更多的时间吗?

】SELECT*真的比只选择需要的列花费更多的时间吗?【英文标题】:DoesSELECT*reallytakemoretimethanselectingonlytheneededcolumns?【发布时间】:2014-04-1814:31:48【问题描述】:它会对网站页面的加载时间产生明显的影响吗?平均而言,我的表... 查看详情

Java 流中 flush() 的目的是啥?

】Java流中flush()的目的是啥?【英文标题】:Whatisthepurposeofflush()inJavastreams?Java流中flush()的目的是什么?【发布时间】:2011-01-2108:35:48【问题描述】:在Java中,flush()方法用于流中。但是我不明白使用这种方法的全部目的是什么?... 查看详情

使用队列时如何在张量流中训练期间测试网络

】使用队列时如何在张量流中训练期间测试网络【英文标题】:Howtotestanetworkduringtrainingintensorflowwhenusingaqueue【发布时间】:2016-11-1219:50:35【问题描述】:我正在使用下面的代码使用队列将我的训练示例提供给我的网络,并且它... 查看详情

保存在 plist 中真的安全吗?

】保存在plist中真的安全吗?【英文标题】:issavinginplistisreallysecure?【发布时间】:2014-09-2922:42:54【问题描述】:我正在开发一个ios游戏,我想知道plist中的保存是否安全?我有一些我真的不知道答案的问题,例如如果用户在保... 查看详情

掌上折扣app是真的吗安全吗

参考技术A真的,安全。根据百度百科查询,掌上折扣app是真的,安全,在正规平台软件上是查得到的。掌上折扣平台内有很多优惠卷,但却并不是一个网购平台,掌上折扣只给用户提供商家发放的商品优惠卷,之后的支付流程... 查看详情

可以考虑将此代码用于 Java 中的成员覆盖吗?

...它就不会被忽视,对吧?这可能是一个愚蠢的问题,但我真的 查看详情

在eclipse中调试单个java测试方法[关闭]

】在eclipse中调试单个java测试方法[关闭]【英文标题】:Debuggingasinglejavatestmethodineclipse[closed]【发布时间】:2021-07-2003:37:46【问题描述】:我有一个包含多个测试方法的测试类,我想挑出一个测试方法来调试。所以第一个问题是:... 查看详情

对于基于 Servlet 的 Java Web 应用程序,我真的需要 web.xml 吗?

】对于基于Servlet的JavaWeb应用程序,我真的需要web.xml吗?【英文标题】:DoIreallyneedweb.xmlforaServletbasedJavawebapplication?【发布时间】:2015-07-2708:34:48【问题描述】:我没有从事过真实世界的网络项目。在大学里,我们同时使用Servlet... 查看详情

我真的将 GPU 用于 tensorflow 吗?

】我真的将GPU用于tensorflow吗?【英文标题】:AmIreallyusingGPUfortensorflow?【发布时间】:2021-12-1521:13:53【问题描述】:我正在学习神经网络,并尝试使用GPU。我正在使用:Python3.8tensorflow-gpu2.6.0PyCharmPyCharm的Jupiter插件显卡NVIDIA3080TI-12... 查看详情