使用Java 8 Streams API,在调用Collectors.toSet()时可以依赖sorted()吗?

Rob*_*ain 5 java collections java-8 java-stream

这是java.util.stream.CollectorstoSet()方法的实现:

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

我们可以看到,它使用a HashSet和调用add.从HashSet 文档中可以看出,"它不能保证集合的迭代顺序;特别是,它不能保证订单在一段时间内保持不变."

在以下代码中,a ListString流式传输,排序并收集到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);
}
Run Code Online (Sandbox Code Playgroud)

这提供了输出:

class java.util.HashSet

[a, b, c]

输出已排序.我认为这里发生的事情是,虽然HashSet文档提供的合同规定订单不是它提供的,但实现恰好按顺序添加.我想这可能会在未来版本中发生变化/在JVM之间有所不同,而且更明智的做法是做类似的事情Collectors.toCollection(TreeSet::new).

可以sorted()调用时不能依赖Collectors.toSet()

此外,"它不能保证订单在一段时间内保持不变"究竟是什么意思?(我想add,remove基础数组的大小调整?)

alf*_*sin 7

答案是不.将项目添加到集合后,您不能依赖任何订单.来自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();
}
Run Code Online (Sandbox Code Playgroud)

现在,在JDK的早期版本中,即使订单无法保证,您通常也会以相同的插入顺序hashCode()hashCode()获取项目(除非对象的类实现,然后您将获得由此指定的顺序)).要么是对象的创建顺序,要么是对象的调用顺序hashCode().正如@Holgar在下面的评论中提到的,在HotSpot中它是后者.你甚至不能指望它,因为这也有例外,因为序列号不是hashCode生成器中的唯一成分.

我最近听到了Stuart Marks(负责重写Java 9中Collections主要部分的人)的演讲,他说他们已经将随机化添加到集合的迭代顺序(由新的集合工厂创建)在Java 9中.如果你想听到会话,他谈到的部分就会从这里开始- 好的谈话,强烈推荐的方式!

因此,即使您曾经依赖于集合的迭代顺序,一旦转移到Java 9,您应该停止这样做.

所有这一切,如果你需要订单,你应该考虑使用SortedSet, LinkedHashSetTreeSet

  • Stuart在*JavaOne 16*中谈到的随机化仅适用于`JEP 269`集合,即新工厂`Map.of(...)`等返回的集合,而不适用于`HashSet`或`HashMap `保持不变.无论你是对的,没有人应该**依赖当前的行为.它已经在一些JDK发布周期之间发生了变化,而且从Java 8开始它也很少在使用它时发生变化(当达到冲突阈值时,它通过使用平衡树重新组织). (4认同)
  • OP询问维护流管道产生的顺序,这不一定可以通过排序来实现.例如,如果您生成单调的序列(例如通过排序)然后添加噪声,则此变换不可逆,因此您无法派生可在排序集中产生相同顺序的比较器. (2认同)
  • @alfasin:如果你看到了,那是因为身份哈希码是从定时器或序列号生成器生成的.这意味着顺序是对象的实例化顺序或它们的第一个`hashCode`调用的顺序(在HotSpot的情况下:后者).在任何一种情况下,它不一定是插入顺序,例如将它们插入一个`HashSet`,然后以不同的顺序将它们插入另一个`HashSet`,第二个集合将不会反映它们的插入顺序......并且几乎每次都有异常值,因为序号不是唯一的哈希成分 (2认同)

Pio*_*kin 7

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

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

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

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

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

  • 不会保留任何其他排序顺序
  • 不适用于hashCodes不遵循其自然顺序的类
  • 不会持有碰撞的哈希表

而且,这与在sorted()流上调用的事实无关- 它仅仅是由于实现的方式hashCode(),因此是哈希表的排序.因此,问题的简单答案是"不".