为什么 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()
方法的原因之一——因为位置是不可预测的,所以使用起来有问题);
在内部,有默认的 capacity
和 loadfactor
值(在 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]]
我创建了两个集合: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()
方法的原因之一——因为位置是不可预测的,所以使用起来有问题);
在内部,有默认的 capacity
和 loadfactor
值(在 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]]