繁体   English   中英

为什么 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
    }
}

有什么有意义的解释吗?

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实现,例如TreeSetConcurrentSkipListSet

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类使用它们的自然顺序,即使用它们在Comparable接口中定义的compareTo的实现。

navSet = [J1, J2, J3, J4]

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

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

如果要指定排序顺序而不是自然顺序,请将Comparator传递给NavigableSet构造函数。 请参见以下示例,为了简洁起见,我们在其中使用了 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 ]]

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

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

如果您需要具有可预测迭代顺序的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遍地使用相同的hashSet ,那是因为hashCode每次都用于为相同的一组值以相同的方式构建该集合。 但是没有保证特定的顺序(这是没有提供带索引的get()方法的原因之一——因为位置是不可预测的,所以使用会有问题);

在内部,有默认的capacityloadfactor值(在HashSet的 JavaDoc 中进行了解释),它们会影响给定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]

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM