使用peek()修改流元素是否是反模式?


36

假设我有一个事物流,并且想在流中“丰富”它们,我可以使用peek()它,例如:

streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

假设在代码的这一点上对事物进行变异是正确的行为-例如,该thingMutator方法可以将“ lastProcessed”字段设置为当前时间。

但是,peek()在大多数情况下,它的意思是“看起来,但不要碰”。

是使用peek()发生变异流元素的反模式或不明智的?

编辑:

另一种更常规的方法是转换消费者:

private void thingMutator(Thing thing) {
    thing.setLastProcessed(System.currentTimeMillis());
}

到返回参数的函数:

private Thing thingMutator(Thing thing) {
    thing.setLastProcessed(currentTimeMillis());
    return thing;
}

map()改用:

stream.map(this::thingMutator)...

但这会引入敷衍的代码(return),但我不认为它会更清晰,因为您知道peek()返回的对象相同,但是map()乍一看并不清楚它是同一对象。

此外,使用peek()lambda可以突变,但是map()您必须构建火车残骸。比较:

stream.peek(t -> t.setLastProcessed(currentTimeMillis())).forEach(...)
stream.map(t -> {t.setLastProcessed(currentTimeMillis()); return t;}).forEach(...)

我认为peek()版本更清晰,lambda也很明显是变异的,因此没有“神秘的”副作用。同样,如果使用方法引用,并且该方法的名称明确暗含了突变,则该名称也很明显。

就个人而言,我不会回避使用peek()变异-我觉得这很方便。



1
您是否使用peek了动态生成其元素的流?它仍然有效,还是所做的更改丢失了?修改流元素对我来说听起来不可靠。
塞巴斯蒂安·雷德尔

@SebastianRedl我发现它100%可靠。我首先用它来收集处理过程:List<Thing> list; things.stream().peek(list::add).forEach(...);非常方便。最近。我用它来添加发布信息:Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());。我知道还有其他方法可以执行此示例,但是在这里我大大简化了。使用peek()产生更紧凑,更优雅的代码恕我直言。除了可读性,这个问题实际上与您提出的内容有关。安全/可靠吗?
波西米亚


@波西米亚 您是否还在练习这种方法peek?我对stackoverflow也有类似的问题,希望您可以看一下并提供反馈。谢谢。stackoverflow.com/questions/47356992/…–
tsolakp

Answers:


23

没错,英语中的“窥视”一词的意思是“看,但不要碰”。

但是JavaDoc指出:

窥视

流窥视(消费者行为)

返回由该流的元素组成的流,并在从结果流中消耗元素时对每个元素另外执行提供的操作。

关键词:“执行……行动”和“消耗”。JavaDoc非常清楚,我们应该期望peek能够修改流。

但是,JavaDoc还声明:

API注意:

存在此方法主要是为了支持调试,您需要在其中查看流过管道中特定点的元素

这表明它更多地用于观察,例如记录流中的元素。

我从所有这些中得到的是,我们可以使用流中的元素执行操作,但应避免使流中的元素发生变异。例如,继续在对象上调用方法,但尝试避免使对象上的操作发生变异。

至少,我会按照以下方式在您的代码中添加简短的注释:

// Note: this peek() call will modify the Things in the stream.
streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

对于此类评论的有用性存在不同意见,但在这种情况下,我将使用此类评论。


1
如果方法名称清楚地表示一个突变(例如thingMutator或更具体的resetLastProcessed等),那么为什么需要注释。除非有令人信服的理由,否则诸如您的建议之类的注释通常表示变量和/或方法名称的选择不佳。如果选择好名字,还不够吗?还是您是说尽管名字很美,但其中的任何内容peek()都像大多数程序员会“跳过”(在视觉上跳过)的盲点一样?另外,“主要用于调试”和“仅用于调试”是不一样的-除了“主要”用途以外,还有什么用途?
Bohemian

@Bohemian我会解释这主要是因为方法名称不直观。而且我不为Oracle工作。我不在他们的Java团队中。我不知道他们打算什么。

@Bohemian因为在寻找以某种方式修改元素的方式时,我不喜欢在“ peek”停止阅读该行,因为“ peek”没有任何改变。只有到了以后,当所有其他代码都没有包含我要追查的错误时,我才可以回到这个问题上来,也许他配备了调试器,然后也许“ omg,谁把这个令人误解的代码放在那里?!”
Frank Hopkins

@Bohemian在这里的示例中,所有内容都在一行中,无论如何都需要阅读,但是可能会跳过“ peek”的内容,并且流声明经常跨越多行,每行执行一次流操作
Frank Hopkins

1

它很容易被误解,所以我会避免像这样使用它。最好的选择可能是使用lambda函数将所需的两个动作合并到forEach调用中。您可能还需要考虑返回一个新对象,而不是对现有对象进行突变-它的效率可能会略低,但它可能会导致代码更具可读性,并减少了意外地将修改后的列表用于应执行的另一操作的可能性已收到原件。


-2

API说明告诉我们,此方法主要是为调试/记录/触发统计信息等操作添加的。

@apiNote存在此方法主要是为了支持调试,您需要在其中*看到流过管道中特定点的元素:*

       {@码
     * Stream.of(“一个”,“两个”,“三个”,“四个”)
     * .filter(e-> e.length()> 3)
     * .peek(e-> System.out.println(“过滤值:” + e))
     * .map(String :: toUpperCase)
     * .peek(e-> System.out.println(“ Mapped value:” + e))
     * .collect(Collectors.toList());
     *}


2
您的答案已经包含在Snowman's中。
文森特·萨瓦德

3
“主要”并不意味着“仅”。
波西米亚

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.