Java 8 findFirst并遇到订单

kou*_*sen 22 java java-8

对JavaDoc中findFirst说,如果流有一个邂逅的命令,那么第一个元素总是会返回,但如果流没有遭遇订单,可以返回的任何元素.

我试图演示它如何在没有遭遇顺序的流上工作,但我不能让它返回除了实际的第一个元素之外的任何东西.

我尝试将元素添加到a 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);
Run Code Online (Sandbox Code Playgroud)

每次我跑,我得到a第一个字符串.然后我试图做一个Collections.shuffle关于List它添加到之前Set,但这并没有改变任何东西.

    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);
Run Code Online (Sandbox Code Playgroud)

a每次都会回复这个词.

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

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

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

Hol*_*ger 22

好吧,"任何"包括"第一"的可能性.当然,Stream实现不会浪费随机化数据的工作,所以对于很多情况,特别是顺序执行,它仍然是第一个元素,如果我们可以这样调用它(因为没有命令,有没有尊贵的第一元素).

展示不同结果的最佳机会findFirst是使用并行Streams.但即便如此,并非每种操作组合都适合展示无序性.

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

这可能会让人大吃一惊,但这HashSet在一定程度上也适用于某种程度.虽然它有一个未指定的顺序,但在某个时间点它的支持数组中会有一个实际的顺序,只要你不修改它Set,就没有理由将这些条目随机改变,所以对于一个特定的HashSet实例,您可以重复获取相同的"第一个"元素,尽管未指定哪个元素,甚至在单个运行时内,另一个HashSet表示相同内容但具有不同历史记录的实例可能具有不同的顺序.


已知从无序特征中获益的操作的一个示例是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);
Run Code Online (Sandbox Code Playgroud)

这会创建一堆equal但可区分的String实例(您通常不应该这样做),将它们的位置编号注册到一个中IdentityHashMap,这样我们就可以找到distinct保留的实例.由于上面的代码使用由a创建的有序流List,因此0无论您多久执行一次,它都会一直打印.

相反,

equal.parallelStream().unordered().distinct().map(map::get)
     .findFirst().ifPresent(System.out::println);
Run Code Online (Sandbox Code Playgroud)

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


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

  • @Federico Peralta Schaffner:别担心,我只是意味着一个伟大的更新回合,没有删除. (3认同)

Stu*_*rks 10

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

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

我们创建另一组单词,添加一堆东西(无论它究竟是什么),然后将其删除:

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

如果我们检查结果如下:

    System.out.println(words.equals(words2));
    System.out.println(words);
    System.out.println(words2);
Run Code Online (Sandbox Code Playgroud)

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

true
[a, strings, stream, of, this, is]
[this, is, strings, stream, of, a]
Run Code Online (Sandbox Code Playgroud)

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

发生了什么,通过添加和删除一堆元素,我们已经导致集合增加其内部表大小,需要重新元素的元素.即使在删除了新元素之后,原始元素也会在新表中的不同相对位置结束.

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

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

  • @StuartMarks在jdk-9中也在其他地方完成了这种随机化吗?运行此代码`Set <String> set = new HashSet <>(); set.add( "A"); set.add( "B"); set.add( "C"); set.add( "d"); for(;;){String result = set.stream().parallel().findFirst().get(); if(!result.equals("a")){System.out.println(result); break用jdk-8冻结,但用jdk-9快速停止(结果!="a").请注意,我没有使用新的不可变集. (3认同)
  • 作为附录,当存在桶冲突但是桶中的元素少于"TREEIFY_THRESHOLD"时,两个相同的`HashSet`可能具有不同的顺序,即使没有不同的容量.在这种情况下,存储桶中的元素将反映插入顺序.当存储桶中的元素数量介于"UNTREEIFY_THRESHOLD"和"TREEIFY_THRESHOLD"之间时,我们甚至可能遇到一个带有树形化存储桶的实例,另一个带有相同内容的链表存储桶.此外,当与非`Comparable`元素存在真正的哈希冲突时,即使在树中,它们也将位于链表中. (3认同)
  • @Eugene 没有对任何现有集合进行随机化。在 JDK 9 中,`HashSet` 迭代顺序没有改变。当然,无序流上的 findFirst 的结果是没有指定的,它可能是不确定的。但是,我看到了您所做的相同行为,即 JDK 8 始终返回 `a`,而 JDK 9 始终返回 `d`。不知道为什么会这样。这可能是因为 fork-join 框架代码中的内部实现更改。它也可能因机器而异。 (2认同)
  • @Holger在桶中拥有更多当时的'TREEIFY_THRESHOLD`元素并不意味着*那个桶中的一棵树.还有`MIN_TREEIFY_CAPACITY`.拿这些字符串(不是随机的):`[ABC,HNI,XZE,KPH,MAW,SCS,GVR,NCX,OEY,NZO]`并将它们作为Key放在HashMap中.将有一个包含10个节点的存储桶,它将*不是*树.我想是一个小小的修正 (2认同)

Eug*_*ene 9

通过将您的流标记为无序,您实际上没有这样做(您没有使您的Set中的顺序有任何不同),而是取消了有序流可能强加的任何限制.

证明这将返回不同结果的方法是使用并行流.

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

运行几次,显示:

  Optional[strings] and then Optional[this]
Run Code Online (Sandbox Code Playgroud)

将您的Set更改为List并且并行运行将保留订单:

 List<String> words = new ArrayList<>();
    words.addAll(Arrays.asList("this", "is", "a", "stream", "of", "strings"));
    Optional<String> firstString = words.stream().parallel()
            .findFirst();
    System.out.println(firstString); // always Optional[this]
Run Code Online (Sandbox Code Playgroud)

这里绝对必读的是霍尔格的大答案

  • @kousen我现在感觉自己像个白痴,我通过jdk-9运行这个代码,它展示了这个行为.确实用jdk-8尝试过它并没有.很奇怪,我没有使用任何新的不可变集合,所以可能其他东西也有变化. (2认同)
  • @kousen是的他确实说过差异,但那是关于*不可变的新集合*; 他指的是像`Map.of(Key,Value)`或`Set.of(E1,E2)`这样的结构.但是在我和我的代码中,我们没有使用任何这些,所以我假设随机化也在其他地方执行. (2认同)