Stream.forEach 是否遵守顺序流的遭遇顺序?

2022-09-01 13:24:22

Stream.forEach 的 Javadoc 说(强调我的):

此操作的行为是显式非确定性的。对于并行流管道,此操作不保证遵循流的遭遇顺序,因为这样做会牺牲并行性的好处。对于任何给定的元素,可以在库选择的任何时间和任何线程中执行操作。如果操作访问共享状态,则它负责提供所需的同步。

Java 9 Early Access Javadoc 中也存在相同的文本。

第一句话(“显式非确定性”)表明(但没有明确说明)此方法不保留遭遇顺序。但是,下一个明确表示不保留顺序的句子以“对于并行流管道”为条件,如果无论并行度如何应用该句子,则该条件将是不必要的。这让我不确定 forEach 是否保留了顺序流的遭遇顺序。

这个答案指出了流库实现调用.sequential().forEach(downstream)的地方。这表明 forEach 旨在保持顺序流的顺序,但也可能只是库中的一个错误。

为了安全起见,我已经在自己的代码中避免了这种歧义,但是今天我发现NetBeans IDE的“使用功能操作”编辑器提示将转换forEachOrdered

for (Foo foo : collection)
    foo.bar();

collection.stream().forEach((foo) -> {
    foo.bar();
});

如果 forEach 不保留遭遇顺序,则会引入一个错误。在针对 NetBeans 报告 bug 之前,我想知道该库在源代码的支持下实际保证了什么。

我正在寻找来自权威来源的答案。这可能是库实现中的明确评论,Java开发邮件列表上的讨论(Google没有为我找到任何东西,但也许我不知道这些神奇的词),或者库设计师的声明(我知道其中两个,Brian GoetzStuart Marks,活跃在Stack Overflow上)。(请不要用“只使用 forEachOrdered 代替”来回答 -- 我已经这样做了,但我想知道没有错误的代码是否错误。


答案 1

规范的存在是为了描述调用方可以依赖的最小保证,而不是描述实现的作用。这一差距至关重要,因为它允许实施灵活性的发展。(规范是声明性的;实现是强制性的。过度指定与规格不足一样糟糕。

当规范说“不保留属性X”时,并不意味着属性X可能永远不会被观察到;这意味着实现没有义务保留它。你声称的暗示,即遭遇顺序永远不会被保留,这只是一个错误的结论。(不保证迭代其元素会保留它们入的顺序,但这并不意味着这不会意外发生 - 你只是不能指望它。HashSet

同样,您暗示 forEach 旨在保持顺序流的顺序“的含义,因为您看到在某些情况下这样做的实现同样不正确。

在这两种情况下,您似乎只是对规范提供了很大的自由度感到不舒服。具体来说,它可以自由地不保留顺序流的遭遇顺序,即使这是实现当前所做的,而且很难想象一个实现会不厌其烦地处理顺序源。但这就是规范所说的,这就是它想要说的。forEach

也就是说,关于并行流的评论的措辞可能会令人困惑,因为仍然有可能误解它。在这里明确指出平行案例的意图是教学性的。完全删除该句子后,规范仍然非常清晰。然而,对于一个不了解并行性的读者来说,几乎不可能假设这将保持相遇顺序,所以添加这句话是为了帮助澄清动机。但是,正如你所指出的,专门处理顺序案例的愿望仍然如此强烈,以至于进一步澄清将是有益的。forEach


答案 2

推荐