关键词:
【中文标题】在 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() 必须返回一个新的帐户对象。这意味着地图操作可以用于登录而不是窥视。
【讨论】:
我们真的需要在 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... 查看详情