Java 8 查找第一和遇到顺序

2022-09-01 18:35:36

findFirst 的 JavaDocs 表示,如果流有遭遇顺序,则始终返回第一个元素,但如果流没有遭遇顺序,则可能会返回任何元素。

我试图演示这如何在没有遇到顺序的流上工作,但我无法让它返回除实际第一个元素之外的任何内容。

我尝试将元素添加到 没有定义的遭遇顺序的 中:Set

    Set<String> words = new HashSet<>();
    words.addAll(Arrays.asList("this", "is", "a", "stream", "of", "strings"));
    Optional<String> firstString = words.stream()
            .findFirst();
    System.out.println(firstString);

每次我跑步时,我都会得到第一个字符串。然后我尝试在将其添加到之前在 上做一个,但这并没有改变任何东西。aCollections.shuffleListSet

    List<String> wordList = Arrays.asList("this", "is", "a", "stream", "of", "strings");
    words = new HashSet<>();
    words.addAll(wordList);
    firstString = words.stream()
            .findFirst();
    System.out.println(firstString);

我仍然每次都找回这个词。a

然后我尝试使用 from 的方法,该方法声称返回没有遇到顺序的流,但没有区别:unorderedBaseStream

    firstString = Stream.of("this", "is", "a", "stream", "of", "strings")
            .unordered()
            .findFirst();
    System.out.println(firstString);

现在我每次都明白这个词。我错过了什么吗?有没有办法证明在无序流上返回不同的值?thisfindFirst


答案 1

好吧,“任何”包括“第一”的可能性。当然,Stream实现不会浪费在随机化数据方面的工作,因此对于很多情况,特别是对于顺序执行,如果我们能这样称呼它,它仍然是第一个元素(因为没有顺序,就没有可区分的第一个元素)。

您展示不同结果的最佳机会是并行流。但即使在那里,也不是每个操作组合都适合表现出无序性。findFirst

有一点是,在当前实现中,当 Stream 是无序的时,操作不会改变它的行为,即它不会主动尝试像 .由于 Stream 的,它可能仍然表现出不可预测的行为,但是如果您的源是 ,即已知大小的不可变序列,则它已经具有最佳的并行性能,因此根本无法从链接中获取好处,因此,当前的实现不会改变其行为。findFirst()findAny()Stream.of("this", "is", "a", "stream", "of", "strings")unordered()

这可能会令人惊讶,但这甚至在某种程度上适用。虽然它有一个未指定的顺序,但在某个时间点,其后备数组中会有一个实际的顺序,只要你不修改,就没有理由在这些条目周围随机排列,所以对于特定实例,你可能会重复获得相同的“first”元素,尽管它没有指定哪一个,甚至在单个运行时内, 表示相同内容但具有不同历史记录的另一个实例可能具有不同的顺序。HashSetSetHashSetHashSet


已知从无序特征中获取好处的操作的一个示例是 。虽然它必须整理重复项,但如果它产生显着差异,它必须保留第一次遇到的相等元素。这可能会显著降低性能,因此,如果流是无序的,则实现将立即尝试获得好处。例如:distinct

List<String> equal=IntStream.range(0, 100)
    .mapToObj(i->new String("test")) // don't do this in normal code
    .collect(Collectors.toList());
Map<String, Integer> map = IntStream.range(0, equal.size())
    .collect(IdentityHashMap::new, (m,i)->m.put(equal.get(i),i), Map::putAll);

equal.parallelStream().distinct().map(map::get)
     .findFirst().ifPresent(System.out::println);

这会创建一堆但可区分的实例(通常不应该这样做),将它们与它们的位置号一起注册为,以便我们可以找出哪个实例已保留。由于上面的代码使用由 a 创建的有序流,因此无论你执行的频率如何,它都会一致地打印。equalStringIdentityHashMapdistinctList0

相比之下,

equal.parallelStream().unordered().distinct().map(map::get)
     .findFirst().ifPresent(System.out::println);

将打印范围的任意数字,因为我们已经发布了有序合约,并允许选择任何相等的字符串。


如前所述,这都是特定于实现的。您永远不应该假设某个操作是否实际上可以获得好处,从而改变其对无序流的行为。上面的解释只是为了说明为什么有时特定实现的行为可能不会在无序流中改变。不过,它仍然可能在下一个版本或不同的JRE实现中。


答案 2

霍尔格已经干练地解释了这种情况。(+1)我想提供具有相同内容但具有不同迭代顺序的实例的演示。首先,我们像以前一样创建一个集合:HashSet

    List<String> wordList = Arrays.asList("this", "is", "a", "stream", "of", "strings");
    Set<String> words = new HashSet<>(wordList);

我们创建另一组单词,添加一堆东西(它到底是什么并不重要),然后删除它:

    Set<String> words2 = new HashSet<>(wordList);
    IntStream.range(0, 50).forEachOrdered(i -> words2.add(String.valueOf(i)));
    words2.retainAll(wordList);

如果我们按如下方式检查结果:

    System.out.println(words.equals(words2));
    System.out.println(words);
    System.out.println(words2);

我们从输出中可以看出,集合是相等的,但以不同的顺序迭代:

true
[a, strings, stream, of, this, is]
[this, is, strings, stream, of, a]

如其他地方所述,如果您从这些元素中获取流并调用 ,则结果是迭代顺序中的第一个元素,这些元素在这些集合之间明显不同。findFirst()

发生的事情是,通过添加和删除一堆元素,我们导致集合增加了其内部表的大小,需要重新哈希元素。原始元素最终会在新表中处于不同的相对位置,即使在新元素被删除后也是如此。

尽管没有指定的迭代顺序,但如果每次都以相同的方式使用相同的内容初始化集合,则该顺序可能是可重复的(甚至是可预测的)。因此,我们说来自集合的流没有定义的遭遇顺序,即使每次的顺序通常都是相同的。HashSets

请注意,在 JDK 9 中,新的不可变集(和映射)实际上是随机的,因此它们的迭代顺序将从运行到运行而变化,即使它们每次都以相同的方式初始化。


推荐