为什么 HashSet 中的项总是以相同的顺序显示?

Why are the items in the HashSet always displayed in the same order?

我创建了两个集合:HashSet 和不可修改的集合。两种类型的集合都不保证元素的顺序。但我注意到,在 hashset 的情况下,结果总是相同的:

 @Test
void displaySets() {
    Set<String> hashSet = new HashSet<>();
    hashSet.add("J1");
    hashSet.add("J2");
    hashSet.add("J3");
    hashSet.add("J4");
    for(String el : hashSet) {
        System.out.println(el); // always the same order - J1, J2, J3, J4
    }

    System.out.println("----------------");

    Set<String> set = Set.of("J1", "J2", "J3", "J4");
    for(String el : set) {
        System.out.println(el); // random order
    }
}

有什么有意义的解释吗?

它们在基本情况下可能在您的系统上以一致的顺序显示,但在另一个系统上或在复杂情况下则不会。

所以最好尊重某些行为无法保证的警告。

“不保证元素的顺序”(documentation中的实际措辞是“它不保证集合的迭代顺序;特别是,它不保证顺序将随着时间的推移保持不变。”)并不意味着“顺序是随机的”。意思是“不要依赖顺序”。

作为推论“不要假设元素不会处于某些顺序”。

如果您需要具有可预测迭代顺序的 Set,请使用 LinkedHashSet

如果您想要(伪)随机顺序,请将其转换为 List 并打乱顺序,如下所示:

Set<String> hashSet = new HashSet<>();
hashSet.add("J1");
hashSet.add("J2");
hashSet.add("J3");
hashSet.add("J4");
List<String> toList = new ArrayList<>(hashSet);
Collections.shuffle(toList);

为什么 HashSet 中的项目总是以相同的顺序显示?

如果您一遍又一遍地表示相同的 hashSet,那是因为 hashCode 每次都以相同的方式为同一组值构建集合。但是不能保证特定的顺序(这是没有提供带有索引的 get() 方法的原因之一——因为位置是不可预测的,所以使用起来有问题);

在内部,有默认的 capacityloadfactor 值(在 JavaDoc 中对 HashSet 进行了解释)可以影响给定 HashSet 的最终顺序.但是这些可以作为参数传递给 HashSet 构造函数。示例如下:

Set<Integer> set = new HashSet<>();
set.addAll(Set.of(1,3,4,2,10,9,28,5,6));
System.out.println(set);
System.out.println(set);
System.out.println(set);

Set<Integer> set2 = new HashSet<>(2, 3f);
set2.addAll(set);
System.out.println(set2);

版画

[1, 2, 3, 4, 5, 6, 9, 10, 28]
[1, 2, 3, 4, 5, 6, 9, 10, 28]
[1, 2, 3, 4, 5, 6, 9, 10, 28]
[4, 28, 1, 5, 9, 2, 6, 10, 3]

Set.of 故意打乱迭代

实际上,Set.of 的迭代行为在 OpenJDK 实现的较新版本中已更改,以便在每次使用时任意更改顺序。早期版本确实在连续使用中保持固定的迭代顺序。

Set < String > set = Set.of( "J1" , "J2" , "J3" , "J4" );
System.out.println( set );

示例运行多次使用该代码:

[J2, J1, J4, J3]

[J1, J2, J3, J4]

[J2, J1, J4, J3]

这种新的任意更改顺序行为旨在训练程序员不要依赖任何特定的顺序。这种新行为强化了 Java 文档中的内容:预计没有特定顺序。

那么为什么 HashSet class' 迭代顺序的行为也没有改变为洗牌行为?我可以想象两个原因:

  • HashSet is much older, arriving in Java 2. Decades of software has been written using that class. Presumably some of that code incorrectly expects a certain ordering. Changing that behavior unnecessarily now would be obnoxious. In contrast, Set.of 在其行为发生变化时相对较新且未使用。
  • Set.of 很可能会随着 Java 的发展而改变,以便在多种实现中进行选择。实现的选择可能取决于收集的对象的种类,并且可能取决于编译时或 运行 时条件。例如,如果使用 Set.of 收集枚举对象,则可以选择 EnumSet class 作为返回的底层实现。这些不同的底层实现可能在它们的迭代顺序行为上有所不同。因此,现在向程序员强调不要依赖今天实现的行为是有道理的,因为明天可能会带来其他实现。

请注意,我小心地避免使用“随机化”一词,而是选择使用“洗牌”。这很重要,因为您甚至不应该依赖真正随机化的 Set 的迭代顺序。始终将任何 Set 对象的迭代视为 任意 (并且可能会发生变化)。

可预测的迭代顺序 NavigableSet/SortedSet

如果您想要特定的迭代顺序,请使用 NavigableSet/SortedSet implementation such as TreeSet or ConcurrentSkipListSet

NavigableSet < String > navSet = new TreeSet <>();
navSet.add( "J3" );
navSet.add( "J1" );
navSet.add( "J4" );
navSet.add( "J2" );

System.out.println( "navSet = " + navSet.toString() );

当 运行 时,我们看到那些 String 对象按字母顺序排序。当我们将每个 String 对象添加到集合中时,TreeSet class 使用它们在 Comparable 接口中定义的 natural ordering, that is, used their implementation of compareTo

navSet = [J1, J2, J3, J4]

顺便说一下,如果你想要两者的优点,TreeSet 的排序和 Set.of 的方便的简短语法,你可以将它们结合起来。 Set 实现的构造函数,例如 TreeSet 允许您传递现有集合。

Set < String > set = new TreeSet <>( Set.of( "J3" , "J1" , "J4" , "J2" ) );

如果要指定排序顺序而不是自然顺序,请传递 Comparator to the NavigableSet constructor. See the following example, where we use the Java 16 feature of records for brevity. Our Comparator implementation is based on the getter method for the date hired, so we get a list of people by seniority. This works because the LocalDate class implements Comparable, and so has a compareTo 方法。

record Person(String name , LocalDate whenHired) {}
Set < Person > navSet = new TreeSet <>(
        Comparator.comparing( Person :: whenHired )
);
navSet.addAll(
        Set.of(
                new Person( "Alice" , LocalDate.of( 2019 , Month.JANUARY , 23 ) ) ,
                new Person( "Bob" , LocalDate.of( 2021 , Month.JUNE , 27 ) ) ,
                new Person( "Carol" , LocalDate.of( 2014 , Month.NOVEMBER , 11 ) )
        )
);

当运行:

navSet.toString() ➠ [Person[name=Carol, whenHired=2014-11-11], Person[name=Alice, whenHired=2019-01-23], Person[name=Bob, whenHired=2021-06-27]]