简体   繁体   English

Java 使用TreeSet时的排序原理

[英]Java Principle of sort when using TreeSet

when use iterator to traversal TreeSet, and of course the hashcode() and equals() is overrides, how does the treeset sort all elements as a certain order when the program is running?当使用迭代器遍历TreeSet时,当然hashcode()和equals()是overrides,程序运行时treeset是如何将所有元素按一定顺序排序的呢? I mean, does the sort occur when the program is running at the "Iterator iterator = set.iterator();"?我的意思是,当程序在“Iterator iterator = set.iterator();”处运行时,是否会发生排序?

Here is a example:这是一个例子:

@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;
    }
}

In Java 16 and later, we can shorten your Student class by making it a record .在 Java 16 及更高版本中,我们可以通过记录缩短您的Student class。 Not the point of this Answer, but using record makes the example code briefer.不是这个答案的重点,而是使用record使示例代码更简洁。 In a record, the compiler implicitly creates a constructor, getters, equals & hashCode , and toString .在记录中,编译器隐式创建构造函数、getter、 equals & hashCodetoString

public record Student ( String name , double score ) {}

The TreeSet class does not actually use the hashCode & equals methods you mention in your Question, as commented by Andreas .正如Andreas 所评论的TreeSet class 实际上并没有使用您在问题中提到的hashCodeequals方法。 You must carefully read the TreeSet and Comparable Javadoc.您必须仔细阅读TreeSetComparable Javadoc。

To work with TreeSet , you must either (a) make your class implement Comparable or (b) you must pass a Comparator implementation when instantiating the set.要使用TreeSet ,您必须 (a) 使 class 实现Comparable或 (b) 在实例化集合时必须传递Comparator实现。 We will take the first approach, implementing Comparable on our Student record.我们将采用第一种方法,在我们的Student记录上实施Comparable

That Javadoc for TreeSet and Comparable explains that your compareTo method must be “consistent with equals”. TreeSetComparable的 Javadoc 说明您的compareTo方法必须“与 equals 一致”。 What does that mean?这意味着什么? If two objects being compared are named a and b , then “consistent with equals” means that where a.equals(b) returns true , so must a.compareTo(b) == 0 return true .如果要比较的两个对象被命名为ab ,那么“与 equals 一致”意味着a.equals(b)返回true ,因此a.compareTo(b) == 0返回true

To quote the Javadoc for Comparable :引用 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. 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. Note that null is not an instance of any class, and e.compareTo(null) should throw a NullPointerException even though e.equals(null) returns false.请注意,null 不是任何 class 的实例,即使 e.equals(null) 返回 false,e.compareTo(null) 也应该抛出 NullPointerException。

It is strongly recommended (though not required) that natural orderings be consistent with equals.强烈建议(尽管不是必需的)自然排序与 equals 一致。 This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent with equals.之所以如此,是因为没有显式比较器的排序集(和排序映射)在与自然顺序与等于不一致的元素(或键)一起使用时表现“奇怪”。 In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.特别是,这样的排序集合(或排序映射)违反了集合(或映射)的一般合同,该合同是根据 equals 方法定义的。

For example, if one adds two keys a and b such that (.a.equals(b) && a,compareTo(b) == 0) to a sorted set that does not use an explicit comparator.例如,如果添加两个键 a 和 b 使得 (.a.equals(b) && a,compareTo(b) == 0) 到不使用显式比较器的排序集。 the second add operation returns false (and the size of the sorted set does not increase) because a and b are equivalent from the sorted set's perspective.第二个加法操作返回 false(并且排序集的大小不会增加),因为从排序集的角度来看 a 和 b 是等价的。

Virtually all Java core classes that implement Comparable have natural orderings that are consistent with equals.几乎所有实现 Comparable 的 Java 核心类都具有与 equals 一致的自然顺序。 One exception is java.math.BigDecimal, …一个例外是 java.math.BigDecimal,......

The default implementation on a record for equals is to compare the equality of each contained object. equals记录的默认实现是比较每个包含的 object 的相等性。 So our compareTo should also account for all of the objects contained in our record.所以我们的compareTo也应该考虑到我们记录中包含的所有对象。 In this example, that means two member fields name & score .在此示例中,这意味着两个成员字段namescore

In contrast, your code violated this rule of compareTo being consistent with equals because your compareTo looks only at score while your equals compares score and name.相反,您的代码违反了compareToequals一致的规则,因为您的compareTo只查看分数,而您的equals比较分数名称。

We could write a compareTo method in a style to that seen in your question.我们可以按照您的问题中看到的样式编写compareTo方法。 In modern Java, it makes more sense to use the streams with method references seen in this next code example.在现代 Java 中,将流与下一个代码示例中看到的方法引用一起使用更有意义。 If you are not comfortable with this style, use your old-school style — just be sure to include both score and name in your comparison .如果您对这种风格不满意,请使用您的老派风格 - 只需确保在比较中包括scorename

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 );
    }
}

The code above might look a tad inefficient as we instantiate a new Comparator on each call to compareTo .当我们在每次调用compareTo时实例化一个新的Comparator时,上面的代码可能看起来有点低效。 Firstly, for a few number of items in your collection, the performance cost is likely immaterial.首先,对于您收藏中的少数项目,性能成本可能无关紧要。 Secondly, I would guess that the compiler or JIT will optimize that away — though I am not sure so perhaps someone might care to post a Comment.其次,我猜编译器或JIT会优化掉它——尽管我不确定,所以也许有人会愿意发表评论。 If concerned about performance, you could store the Comparator object in a static field.如果担心性能,您可以将Comparator器 object 存储在static字段中。

And now we move on to the code for using that Student class.现在我们继续使用该Student class 的代码。

Your example code:您的示例代码:

        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());

        }

…has a few problems. ……有一些问题。

You hard-coded a limit of 3 in your for loop rather than soft-code for the size of the collection of Student objects.您在for循环中硬编码了 3 的限制,而不是对Student对象集合的大小进行软编码。 Perhaps you did indeed want only the bottom three objects.也许您确实只想要底部的三个对象。 But I will assume that was a mistake, and you want all the objects to be reported.但我会假设这是一个错误,并且您希望报告所有对象。

You call iterator.hasNext();你调用iterator.hasNext(); . . This serves no purpose in your example.这在您的示例中毫无用处。 Just delete it.只需删除它。

You defined your set as a raw type rather than using generics.您将集合定义为原始类型,而不是使用 generics。 In modern Java your line TreeSet set = new TreeSet();在现代 Java 中,您的行TreeSet set = new TreeSet(); should be TreeSet < Student > set = new TreeSet<>();应该是TreeSet < Student > set = new TreeSet<>(); . . This tells the compiler that we intend to store Student objects in this collection, and only Student objects.这告诉编译器我们打算在这个集合中存储Student对象,并且存储Student对象。 If we try to store an Elephant or Invoice , the compiler complains.如果我们尝试存储ElephantInvoice ,编译器会抱怨。

And I suggest having more specific naming in your variables.我建议在变量中使用更具体的命名。 So students rather than set .所以students而不是set

You define your collection of students as TreeSet .您将学生集合定义为TreeSet No need to do so.没有必要这样做。 Your example code does not explicitly call methods that exist only on TreeSet .您的示例代码没有显式调用仅存在于TreeSet上的方法。 Your code only makes the assumption that the set be a NavigableSet .您的代码仅假设集合是NavigableSet So use the more general interface rather than restricting yourself to the more specific concrete TreeSet .因此,请使用更通用的接口,而不是将自己限制在更具体的具体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() );
}

By the way, for less code, I would be tempted to use Set.of syntax.顺便说一句,对于更少的代码,我很想使用Set.of语法。 The Set.of method returns an unmodifiable set . Set.of方法返回一个不可修改的集合 So we feed that to the constructor of our TreeSet .所以我们将它提供给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() );
}

No need to explicitly instantiate the iterator.无需显式实例化迭代器。 We could more simply use the for-each syntax in modern Java.我们可以更简单地使用现代 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 );
}

When run.跑的时候。

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]

As for your question on:至于你的问题:

how does the treeset sort all elements as a certain order when the program is running?程序运行时,treeset 如何按一定的顺序对所有元素进行排序? I mean, does the sort occur when the program is running at the "Iterator iterator = set.iterator();"?我的意思是,当程序在“Iterator iterator = set.iterator();”处运行时,是否会发生排序?

At first thought, does it matter?乍一看,这有关系吗? All we really care is that the TreeSet class deliver on the promises made by the NavigableSet contract.我们真正关心的是TreeSet class 兑现了NavigableSet合约的承诺。 If we ask for the first one or last one, or ask to iterate, the results should be in the sorted order defined for our particular set's objects Comparable or Comparator implementation.如果我们要求第一个或最后一个,或者要求迭代,结果应该按照为我们特定集合的对象ComparableComparator实现定义的排序顺序。 Generally, we should not care about the internal details of how TreeSet arranges its contents.一般来说,我们不应该关心TreeSet如何排列其内容的内部细节。

But if you really want an answer, read the add method on TreeSet .但是,如果您真的想要答案,请阅读TreeSet上的add方法。 The Javadoc says it throws ClassCastException if the object cannot be compared with the elements currently in the set. Javadoc 说如果 object 不能与当前集合中的元素进行比较,它会抛出ClassCastException So we know the comparison is being made when first adding an object to the collection.因此,我们知道在首次将 object 添加到集合时进行比较。 We can presume a structure is used internally to maintain that order.我们可以假设内部使用了一个结构来维护该顺序。

If you really care about the details, look at the source code in OpenJDK project for the current implementation.如果您真的关心细节,请查看当前实现的 OpenJDK 项目中的源代码 But keep in mind that other implementations of Java are free to write a different implementation of that class.但请记住,Java 的其他实现可以自由编写 class 的不同实现。

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

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