簡體   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