【问题标题】:Using the Java 8 Streams API, can sorted() be relied upon when calling Collectors.toSet()?使用 Java 8 Streams API,调用 Collectors.toSet() 时是否可以依赖 sorted()?
【发布时间】:2017-10-20 21:31:53
【问题描述】:

这是java.util.stream.Collectors类的toSet()方法的实现:

public static <T>
Collector<T, ?, Set<T>> toSet() {
    return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_UNORDERED_ID);
}

正如我们所见,它使用HashSet 并调用add。来自HashSetdocumentation,“它不保证集合的迭代顺序;特别是,它不保证顺序会随着时间的推移保持不变。”

在以下代码中,String 中的 List 被流式传输、排序并收集到 Set 中:

public static void main(String[] args) {
    Set<String> strings = Arrays.asList("c", "a", "b")
            .stream()
            .sorted()
            .collect(Collectors.toSet());
    System.out.println(strings.getClass());
    System.out.println(strings);
}

这提供了输出:

class java.util.HashSet

[a, b, c]

输出已排序。我认为这里发生的事情是,尽管HashSet 文档提供的合同指定排序不是它提供的东西,但实现恰好按顺序添加。我想这可能会在未来的版本中发生变化/在 JVM 之间有所不同,更明智的方法是执行类似 Collectors.toCollection(TreeSet::new) 的操作。

调用Collectors.toSet()时可以依赖sorted()吗?

此外,“它不能保证订单会随着时间的推移保持不变”到底是什么意思? (我想addremove,底层数组的大小调整?)

【问题讨论】:

  • "调用 Collectors.toSet() 时可以依赖 sorted() 吗?"编号For example.
  • 如果需要在不同的 JVM 实例(和/或不同的 JVM 发布周期)中维护任何顺序,则必须使用 LinkedHashSet 或类似的类来确保 确定性 顺序.原因已经在答案中给出。

标签: java collections java-8 java-stream


【解决方案1】:

要回答这个问题,您必须了解HashSet 是如何实现的。顾名思义,HashSet 是使用 散列表 实现的。基本上,哈希表是一个由元素哈希索引的数组。哈希函数(在 Java 中,对象的哈希是由object.hashCode() 计算的)基本上是一个满足几个条件的函数:

  • 计算给定元素的速度(相对)快
  • .equals() 彼此的两个对象具有相同的哈希值
  • 不同项目具有相同哈希的概率很低

所以,当你遇到一个“排序”的HashSet(被理解为“迭代器保留元素的自然顺序”)时,这是由于几个巧合:

  • 元素的自然顺序尊重其hashCodes的自然顺序
  • 哈希表足够小,不会发生冲突(具有相同哈希码的两个元素)

如果您查看 StringhashCode() 方法,您会看到对于单字母字符串,哈希码对应于字母的 Unicode 索引(代码点) - 所以在这种特定情况下,只要由于哈希表足够小,元素将被排序。然而,这是一个巨大的巧合,并且

  • 不适用于任何其他排序顺序
  • 不适用于 hashCode 不遵循其自然顺序的类
  • 不会保存有冲突的哈希表

此外,这与sorted() 在流上被调用这一事实无关——这仅仅是由于hashCode() 的实现方式以及哈希表的排序。因此,这个问题的简单答案是“否”。

【讨论】:

  • 您说得对,结果顺序似乎与排序顺序匹配纯属巧合,但也值得一提(明确地),这与插入顺序无关,即流链中是否存在sorted() 无关紧要。顺便说一句,对于“单字母字符串”,哈希码与它们的 Unicode Codepoint 匹配,而这恰好是“单字母字符串”的 ASCII 索引。
【解决方案2】:

答案是否定的。一旦您将项目添加到 Set 中,您就不能依赖任何顺序。来自JDK源代码(HashSet.java):

/**
 * Returns an iterator over the elements in this set.  The elements
 * are returned in no particular order.
 *
 * @return an Iterator over the elements in this set
 * @see ConcurrentModificationException
 */
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

现在,在以前的 JDK 版本中,即使不能保证顺序,您通常也会以相同的插入顺序获得 中的项目(除非对象的类实现了hashCode(),然后您' 将得到由hashCode() 指定的顺序。 对象的创建顺序或hashCode() 对对象的调用顺序。正如@Holgar 在下面的 cmets 中提到的,在 HotSpot 中是后者。而且你甚至不能指望这一点,因为这也有例外,因为序列号不是 hashCode 生成器中的唯一成分。

我最近听到Stuart Marks(负责在 Java 9 中重写 Collections 的主要部分的人)的演讲,他说他们已经在 Sets 的迭代顺序中添加了随机化(由Java 9 中的新 set-factories)。如果你想听会议,他谈论 set 的部分开始 here - 好话,顺便强烈推荐!

因此,即使您过去依赖于 Set 的迭代顺序,一旦您迁移到 Java 9,您也应该停止这样做。

话虽如此,如果您需要订购,您应该考虑使用SortedSetLinkedHashSetTreeSet

【讨论】:

  • Stuart 在 JavaOne 16 中谈到的随机化仅适用于 JEP 269 集合,即由新工厂返回的集合 Map.of(...) 等,不适用于 @ 987654335@ 或 HashMap 保持不变。不管你是对的,没有人应该依赖当前的行为。它已经在一些 JDK 发布周期之间发生了变化,并且从 Java 8 开始,它在使用它时也很少会发生变化(当达到冲突阈值时,它会通过使用平衡树重新组织自身)。
  • @Zabuza 你是对的,随机化仅添加到新工厂(目前)。
  • SortedSets 不保留插入顺序,只有 LinkedHashSet 保留。
  • OP 询问维护流管道产生的顺序,这不一定可以通过排序来实现。例如如果你产生一个单调的序列(例如通过排序)然后添加噪声,那么这个变换是不可逆的,因此你不能推导出一个可以在排序集中产生相同顺序的比较器。
  • @alfasin:如果你看到了,那是因为身份哈希码是由计时器或序列号生成器生成的。这意味着顺序要么是对象的实例化顺序,要么是它们的第一个 hashCode 调用顺序(在 HotSpot 的情况下:后者)。无论哪种情况,都不一定是插入顺序,例如将它们插入一个HashSet,然后以不同的顺序将它们插入另一个HashSet,第二组不会反映它们的插入顺序……而且几乎每次都有异常值,因为序号不是唯一的哈希成分
猜你喜欢
  • 1970-01-01
  • 2017-09-27
  • 2017-01-23
  • 2020-12-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-20
  • 1970-01-01
相关资源
最近更新 更多