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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/33635717/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 14:51:41  来源:igfitidea点击:

In Java streams is peek really only for debugging?

javajava-8java-streampeek

提问by Adam.J

I'm reading up about Java streams and discovering new things as I go along. One of the new things I found was the peek()function. Almost everything I've read on peek says it should be used to debug your Streams.

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

What if I had a Stream where each Account has a username, password field and a login() and loggedIn() method.

如果我有一个流,其中每个帐户都有一个用户名、密码字段和一个 login() 和 login() 方法。

I also have

我也有

Consumer<Account> login = account -> account.login();

and

Predicate<Account> loggedIn = account -> account.loggedIn();

Why would this be so bad?

为什么会如此糟糕?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Now as far as I can tell this does exactly what it's intended to do. It;

现在据我所知,这完全符合它的意图。它;

  • Takes a list of accounts
  • Tries to log in to each account
  • Filters out any account which aren't logged in
  • Collects the logged in accounts into a new list
  • 获取帐户列表
  • 尝试登录每个帐户
  • 过滤掉任何未登录的帐户
  • 将登录的帐户收集到一个新列表中

What is the downside of doing something like this? Any reason I shouldn't proceed? Lastly, if not this solution then what?

做这样的事情有什么缺点?有什么理由我不应该继续?最后,如果不是这个解决方案,那又是什么?

The original version of this used the .filter() method as follows;

这个的原始版本使用了 .filter() 方法,如下所示;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

采纳答案by Makoto

The key takeaway from this:

关键要点:

Don't use the API in an unintended way, even if it accomplishes your immediate goal.That approach may break in the future, and it is also unclear to future maintainers.

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



There is no harm in breaking this out to multiple operations, as they are distinct operations. There isharm in using the API in an unclear and unintended way, which may have ramifications if this particular behavior is modified in future versions of Java.

将其分解为多个操作没有坏处,因为它们是不同的操作。还有就是在不明确的和意想不到的方式,如果这种特定的行为是用Java的未来版本中修改其可能的后果使用API的伤害。

Using forEachon this operation would make it clear to the maintainer that there is an intendedside effect on each element of accounts, and that you are performing some operation that can mutate it.

使用forEach该操作将明确维护者,有一个预期中的每个元素的副作用accounts,并且正在执行某些操作,可能发生变异它。

It's also more conventional in the sense that peekis an intermediate operation which doesn't operate on the entire collection until the terminal operation runs, but forEachis indeed a terminal operation. This way, you can make strong arguments around the behavior and the flow of your code as opposed to asking questions about if peekwould behave the same as forEachdoes in this context.

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

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

回答by Holger

The important thing you have to understand is that streams are driven by the terminal operation. The terminal operation determines whether all elements have to be processed or any at all. So collectis an operation that processes each item, whereas findAnymay stop processing items once it encountered a matching element.

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

And count()may not process any elements at all when it can determine the size of the stream without processing the items. Since this is an optimization not made in Java?8, but which will be in Java?9, there might be surprises when you switch to Java?9 and have code relying on count()processing all items. This is also connected to other implementation-dependent details, e.g. even in Java?9, the reference implementation will not be able to predict the size of an infinite stream source combined with limitwhile there is no fundamental limitation preventing such prediction.

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

Since peekallows “performing the provided action on each element as elements are consumed from the resulting stream”, it does not mandate processing of elements but will perform the action depending on what the terminal operation needs. This implies that you have to use it with great care if you need a particular processing, e.g. want to apply an action on all elements. It works if the terminal operation is guaranteed to process all items, but even then, you must be sure that not the next developer changes the terminal operation (or you forget that subtle aspect).

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

Further, while streams guarantee to maintain the encounter order for a certain combination of operations even for parallel streams, these guarantees do not apply to peek. When collecting into a list, the resulting list will have the right order for ordered parallel streams, but the peekaction may get invoked in an arbitrary order and concurrently.

此外,虽然流保证即使对于并行流也能保持特定操作组合的相遇顺序,但这些保证不适用于peek。当收集到一个列表中时,结果列表将具有有序并行流的正确顺序,但该peek操作可能以任意顺序并发调用。

So the most useful thing you can do with peekis to find out whether a stream element has been processed which is exactly what the API documentation says:

因此,您可以做的最有用的事情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

此方法的存在主要是为了支持调试,您希望在其中查看元素流经管道中的某个点时的情况

回答by chimera8

Perhaps a rule of thumb should be that if you do use peek outside the "debug" scenario, you should only do so if you're sure of what the terminating and intermediate filtering conditions are. For example:

也许经验法则应该是,如果您确实在“调试”场景之外使用了 peek,则只有在您确定终止和中间过滤条件是什么时才应该这样做。例如:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

seems to be a valid case where you want, in one operation to transform all Foos to Bars and tell them all hello.

似乎是您想要的有效案例,在一次操作中将所有 Foo 转换为 Bar 并告诉他们大家好。

Seems more efficient and elegant than something like:

似乎比以下内容更高效、更优雅:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

and you don't end up iterating a collection twice.

并且您最终不会对集合进行两次迭代。

回答by UltimaWeapon

Although I agree with most answers above, I have one case in which using peek actually seems like the cleanest way to go.

尽管我同意上面的大多数答案,但我有一个案例,其中使用 peek 实际上似乎是最干净的方法。

Similar to your use case, suppose you want to filter only on active accounts and then perform a login on these accounts.

与您的用例类似,假设您只想过滤活动帐户,然后对这些帐户执行登录。

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek is helpful to avoid the redundant call while not having to iterate the collection twice:

Peek 有助于避免重复调用,而不必重复收集两次:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

回答by Marinos An

I would say that peekprovides the ability to decentralize code that can mutate stream objects, or modify global state(based on them), instead of stuffing everything into a simple or composed functionpassed to a terminal method.

我会说它peek提供了去中心化代码的能力,这些代码可以改变流对象,或者修改全局状态(基于它们),而不是把所有东西都塞进传递给终端方法的简单或组合函数中

Now the question might be: should we mutate stream objects or change global state from within functions in functional style java programming?

现在的问题可能是:在函数式 Java 编程中,我们应该改变流对象还是从函数内部改变全局状态

If the answer to any of the the above 2 questions is yes (or: in some cases yes) then peek()is definitely not only for debugging purposes, for the same reason that forEach()isn't only for debugging purposes.

如果回答任何的上述2个问题是肯定的(或:在某些情况下是),则peek()绝对不仅是为了调试的目的对于同样的原因,forEach()不仅是为了调试的目的

For me when choosing between forEach()and peek(), is choosing the following: Do I want pieces of code that mutate stream objects to be attached to a composable, or do I want them to attach directly to stream?

对我来说,在forEach()和之间peek()进行选择时,是选择以下内容:我是否希望将改变流对象的代码片段附加到可组合对象,还是希望它们直接附加到流?

I think peek()will better pair with java9 methods. e.g. takeWhile()may need to decide when to stop iteration based on an already mutated object, so paring it with forEach()would not have the same effect.

我认为peek()将更好地与 java9 方法配对。例如,takeWhile()可能需要根据已经发生变异的对象来决定何时停止迭代,因此forEach()将其与它配对不会产生相同的效果。

P.S.I have not referenced map()anywhere because in case we want to mutate objects (or global state), rather than generating new objects, it works exactly like peek().

PS我没有map()在任何地方引用,因为如果我们想要改变对象(或全局状态),而不是生成新对象,它的工作方式与peek().

回答by Solubris

The functional solution is to make account object immutable. So account.login() must return a new account object. This will mean that the map operation can be used for login instead of peek.

功能解决方案是使帐户对象不可变。所以 account.login() 必须返回一个新的帐户对象。这意味着 map 操作可以用于登录而不是 peek。