2

我创建了两个集合: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
    }
}

有什么有意义的解释吗?

4

4 回答 4

2

Set.of迭代故意改组

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

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]

这种新的任意更改顺序行为旨在训练程序员不要依赖任何特定的顺序。这种新行为强化了 Javadoc 中的说明:期望没有特定的顺序。

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

  • HashSet更老,在 Java 2 中出现。使用该类编写了数十年的软件。据推测,其中一些代码错误地期望某种顺序。现在不必要地改变这种行为将是令人讨厌的。相比之下,Set.of在其行为发生变化时,它相对较新且未使用。
  • Set.of可能会随着 Java 的发展而改变,以便在多个实现中进行选择。实现的选择可以取决于被收集的对象的种类,并且可以取决于编译时或运行时条件。例如,如果使用 收集枚举对象Set.of,则EnumSet可以选择该类作为返回的底层实现。这些不同的底层实现可能在它们的迭代顺序行为上有所不同。因此,当明天很可能会带来其他实现时,现在向程序员强调不要依赖今天实现的行为是有意义的。

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

可预测的迭代顺序与NavigableSet/SortedSet

如果您想要特定的迭代顺序,请使用NavigableSet/SortedSet实现,例如TreeSetor 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该类使用它们的自然顺序,即使用它们在接口中compareTo定义的实现。Comparable

导航集 = [J1,J2,J3,J4]

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

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

如果要指定排序顺序而不是自然顺序,请将 a 传递ComparatorNavigableSet构造函数。请参阅以下示例,为简洁起见,我们使用 Java 16记录功能。我们的Comparator实现基于聘用日期的 getter 方法,因此我们按资历获取人员列表。这是有效的,因为LocalDate该类实现了Comparable,因此有一个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 ]]

于 2021-06-28T20:14:56.017 回答
1

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

作为推论,“不要假设元素不会某种顺序排列”。

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

如果您希望它以(伪)随机顺序,请将其转换为 aList并随机播放,如下所示:

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);
于 2021-06-28T17:26:26.997 回答
0

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

因此,最好尊重某些行为无法保证的警告。

于 2021-06-28T17:26:07.043 回答
0

为什么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]
于 2021-06-28T17:26:53.693 回答