繁体   English   中英

Java 使用TreeSet时的排序原理

[英]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 & hashCodetoString

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

正如Andreas 所评论的TreeSet class 实际上并没有使用您在问题中提到的hashCodeequals方法。 您必须仔细阅读TreeSetComparable Javadoc。

要使用TreeSet ,您必须 (a) 使 class 实现Comparable或 (b) 在实例化集合时必须传递Comparator实现。 我们将采用第一种方法,在我们的Student记录上实施Comparable

TreeSetComparable的 Javadoc 说明您的compareTo方法必须“与 equals 一致”。 这意味着什么? 如果要比较的两个对象被命名为ab ,那么“与 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也应该考虑到我们记录中包含的所有对象。 在此示例中,这意味着两个成员字段namescore

相反,您的代码违反了compareToequals一致的规则,因为您的compareTo只查看分数,而您的equals比较分数名称。

我们可以按照您的问题中看到的样式编写compareTo方法。 在现代 Java 中,将流与下一个代码示例中看到的方法引用一起使用更有意义。 如果您对这种风格不满意,请使用您的老派风格 - 只需确保在比较中包括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 );
    }
}

当我们在每次调用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对象。 如果我们尝试存储ElephantInvoice ,编译器会抱怨。

我建议在变量中使用更具体的命名。 所以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合约的承诺。 如果我们要求第一个或最后一个,或者要求迭代,结果应该按照为我们特定集合的对象ComparableComparator实现定义的排序顺序。 一般来说,我们不应该关心TreeSet如何排列其内容的内部细节。

但是,如果您真的想要答案,请阅读TreeSet上的add方法。 Javadoc 说如果 object 不能与当前集合中的元素进行比较,它会抛出ClassCastException 因此,我们知道在首次将 object 添加到集合时进行比较。 我们可以假设内部使用了一个结构来维护该顺序。

如果您真的关心细节,请查看当前实现的 OpenJDK 项目中的源代码 但请记住,Java 的其他实现可以自由编写 class 的不同实现。

暂无
暂无

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

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