[英]Java Principle of sort when using TreeSet
当使用迭代器遍历TreeSet时,当然hashcode()和equals()是overrides,程序运行时treeset是如何将所有元素按一定顺序排序的呢? 我的意思是,当程序在“Iterator iterator = set.iterator();”处运行时,是否会发生排序?
这是一个例子:
@Test
public void test(){
TreeSet set = new TreeSet();
set.add(new Student2("Sam",97.8));
set.add(new Student2("Joe",95.8));
set.add(new Student2("Ben",99));
set.add(new Student2("Chandler",93));
set.add(new Student2("Ross",100));
Iterator iterator = set.iterator();
for (int i = 0; i < 3; i++) {
iterator.hasNext();
System.out.println(iterator.next());
}
}
public class Student2 implements Comparable {
private String name;
private double score;
public Student2(String name, double score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Student2{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Student2 student2 = (Student2) o;
return Double.compare(student2.score, score) == 0 && Objects.equals(name, student2.name);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), name, score);
}
@Override
public int compareTo(Object o) {
if(this != null){
if(o instanceof Student2){
Student2 s = (Student2) o;
int i = (int) (s.score - this.score);
return i;
}else{
throw new RuntimeException("Wrong type");
}
}
return 0;
}
}
在 Java 16 及更高版本中,我们可以通过记录缩短您的Student
class。 不是这个答案的重点,而是使用record
使示例代码更简洁。 在记录中,编译器隐式创建构造函数、getter、 equals
& hashCode
和toString
。
public record Student ( String name , double score ) {}
正如Andreas 所评论的, TreeSet
class 实际上并没有使用您在问题中提到的hashCode
和equals
方法。 您必须仔细阅读TreeSet
和Comparable
Javadoc。
要使用TreeSet
,您必须 (a) 使 class 实现Comparable
或 (b) 在实例化集合时必须传递Comparator
实现。 我们将采用第一种方法,在我们的Student
记录上实施Comparable
。
TreeSet
和Comparable
的 Javadoc 说明您的compareTo
方法必须“与 equals 一致”。 这意味着什么? 如果要比较的两个对象被命名为a
和b
,那么“与 equals 一致”意味着a.equals(b)
返回true
,因此a.compareTo(b) == 0
返回true
。
引用 Javadoc 的Comparable
:
The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C. 请注意,null 不是任何 class 的实例,即使 e.equals(null) 返回 false,e.compareTo(null) 也应该抛出 NullPointerException。
强烈建议(尽管不是必需的)自然排序与 equals 一致。 之所以如此,是因为没有显式比较器的排序集(和排序映射)在与自然顺序与等于不一致的元素(或键)一起使用时表现“奇怪”。 特别是,这样的排序集合(或排序映射)违反了集合(或映射)的一般合同,该合同是根据 equals 方法定义的。
例如,如果添加两个键 a 和 b 使得 (.a.equals(b) && a,compareTo(b) == 0) 到不使用显式比较器的排序集。 第二个加法操作返回 false(并且排序集的大小不会增加),因为从排序集的角度来看 a 和 b 是等价的。
几乎所有实现 Comparable 的 Java 核心类都具有与 equals 一致的自然顺序。 一个例外是 java.math.BigDecimal,......
equals
记录的默认实现是比较每个包含的 object 的相等性。 所以我们的compareTo
也应该考虑到我们记录中包含的所有对象。 在此示例中,这意味着两个成员字段name
和score
。
相反,您的代码违反了compareTo
与equals
一致的规则,因为您的compareTo
只查看分数,而您的equals
比较分数和名称。
我们可以按照您的问题中看到的样式编写compareTo
方法。 在现代 Java 中,将流与下一个代码示例中看到的方法引用一起使用更有意义。 如果您对这种风格不满意,请使用您的老派风格 - 只需确保在比较中包括score
和name
。
package org.example;
import java.util.Comparator;
public record Student ( String name , double score ) implements Comparable < Student >
{
@Override
public int compareTo ( Student o )
{
return
Comparator
.comparing( Student :: score )
.thenComparing( Student :: name )
.compare( this , o );
}
}
当我们在每次调用compareTo
时实例化一个新的Comparator
时,上面的代码可能看起来有点低效。 首先,对于您收藏中的少数项目,性能成本可能无关紧要。 其次,我猜编译器或JIT会优化掉它——尽管我不确定,所以也许有人会愿意发表评论。 如果担心性能,您可以将Comparator
器 object 存储在static
字段中。
现在我们继续使用该Student
class 的代码。
您的示例代码:
TreeSet set = new TreeSet();
set.add(new Student2("Sam",97.8));
set.add(new Student2("Joe",95.8));
set.add(new Student2("Ben",99));
set.add(new Student2("Chandler",93));
set.add(new Student2("Ross",100));
Iterator iterator = set.iterator();
for (int i = 0; i < 3; i++) {
iterator.hasNext();
System.out.println(iterator.next());
}
……有一些问题。
您在for
循环中硬编码了 3 的限制,而不是对Student
对象集合的大小进行软编码。 也许您确实只想要底部的三个对象。 但我会假设这是一个错误,并且您希望报告所有对象。
你调用iterator.hasNext();
. 这在您的示例中毫无用处。 只需删除它。
您将集合定义为原始类型,而不是使用 generics。 在现代 Java 中,您的行TreeSet set = new TreeSet();
应该是TreeSet < Student > set = new TreeSet<>();
. 这告诉编译器我们打算在这个集合中存储Student
对象,并且只存储Student
对象。 如果我们尝试存储Elephant
或Invoice
,编译器会抱怨。
我建议在变量中使用更具体的命名。 所以students
而不是set
。
您将学生集合定义为TreeSet
。 没有必要这样做。 您的示例代码没有显式调用仅存在于TreeSet
上的方法。 您的代码仅假设集合是NavigableSet
。 因此,请使用更通用的接口,而不是将自己限制在更具体的具体TreeSet
上。
NavigableSet < Student > students = new TreeSet <>();
students.add( new Student( "Sam" , 97.8 ) );
students.add( new Student( "Joe" , 95.8 ) );
students.add( new Student( "Ben" , 99 ) );
students.add( new Student( "Chandler" , 93 ) );
students.add( new Student( "Ross" , 100 ) );
Iterator iterator = students.iterator();
for ( int i = 0 ; i < students.size() ; i++ )
{
System.out.println( iterator.next() );
}
顺便说一句,对于更少的代码,我很想使用Set.of
语法。 Set.of
方法返回一个不可修改的集合。 所以我们将它提供给TreeSet
的构造函数。
NavigableSet < Student > students = new TreeSet <>(
Set.of(
new Student( "Sam" , 97.8 ) ,
new Student( "Joe" , 95.8 ) ,
new Student( "Ben" , 99 ) ,
new Student( "Chandler" , 93 ) ,
new Student( "Ross" , 100 )
)
);
Iterator iterator = students.iterator();
for ( int i = 0 ; i < students.size() ; i++ )
{
System.out.println( iterator.next() );
}
无需显式实例化迭代器。 我们可以更简单地使用现代 Java 中的 for-each 语法。
NavigableSet < Student > students = new TreeSet <>(
Set.of(
new Student( "Sam" , 97.8 ) ,
new Student( "Joe" , 95.8 ) ,
new Student( "Ben" , 99 ) ,
new Student( "Chandler" , 93 ) ,
new Student( "Ross" , 100 )
)
);
for ( Student student : students )
{
System.out.println( student );
}
跑的时候。
Student[name=Chandler, score=93.0]
Student[name=Joe, score=95.8]
Student[name=Sam, score=97.8]
Student[name=Ben, score=99.0]
Student[name=Ross, score=100.0]
至于你的问题:
程序运行时,treeset 如何按一定的顺序对所有元素进行排序? 我的意思是,当程序在“Iterator iterator = set.iterator();”处运行时,是否会发生排序?
乍一看,这有关系吗? 我们真正关心的是TreeSet
class 兑现了NavigableSet
合约的承诺。 如果我们要求第一个或最后一个,或者要求迭代,结果应该按照为我们特定集合的对象Comparable
或Comparator
实现定义的排序顺序。 一般来说,我们不应该关心TreeSet
如何排列其内容的内部细节。
但是,如果您真的想要答案,请阅读TreeSet
上的add
方法。 Javadoc 说如果 object 不能与当前集合中的元素进行比较,它会抛出ClassCastException
。 因此,我们知道在首次将 object 添加到集合时进行比较。 我们可以假设内部使用了一个结构来维护该顺序。
如果您真的关心细节,请查看当前实现的 OpenJDK 项目中的源代码。 但请记住,Java 的其他实现可以自由编写 class 的不同实现。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.