TreeSet有序和無重複值特性的內部原理剖析

此篇用以致敬:那些年,我們一起學過的TreeSet。
相信很多學過Java的小孩子們,都知道TreeSet有兩大特性:來,大聲喊出來

一、有序;二、值唯一

一、寫作背景

有木有感覺好高大

在最初我們學習集合框架時,Collection接口以及他的寶寶List接口和Set接口一定是我們最先開始接觸的。而Set接口是個神奇的接口,爲什麼呢?因爲他有一個淘氣的TreeSet。嗯嘛嘛(之所以淘氣嘛,是因爲博主剛開始自己學不會TreeSet,一下就掉進坑裏,哼哧哼哧爬不上來,啊哈哈哈哈)

二、TreeSet的排序分析

在TreeSet中,默認是升序排序哦

public class testTreeSet {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        set.add(5);
        set.add(4);
        set.add(3);
        set.add(2);
        set.add(1);
        System.out.println(set);
    }
}

輸出結果:

我們來看看,他爲什麼默認是升序?

(1)TreeSet是基於TreeMap實現的;

 

(2)查看TreeSet的add()方法;

在add()方法中,竟然有個m,讓我們來猜猜m是什麼呢?

m是NavigableMap的一個實例;

那麼NavigableMap又是什麼?

不難發現,TreeMap實現了NavigableMap接口

搞清楚m後,繼續回到TreeMap的add()方法中,發現了put(),啊,熟悉的put,爲了查看這個源碼,發現源碼在Map接口中,但是沒有方法體,所以還是去TreeMap中去看,畢竟TreeSet還是基於TreeMap(好像分析了挺久,怎麼回到了(1),不管了,繼續征程。。。)

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
​
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

從put()方法中,閱讀源碼發現,將元素構成了一棵樹,但是不允許有重複元素,
(1)先將第一個元素作爲根節點,
(2)比較已存在結點和新結點的值; 大於根結點,將它置爲右結點; 小於根結點值,將它置爲左結點; 等於根結點值,不存儲;
(3)其他元素按照步驟依次存儲 以5,4,3,2,1爲例,將會構造出這樣一棵樹:

                                  

這樣就剖析結束了嗎,吶,按照劇情走,還有。。。

在仔細觀察put()後,發現在比較值時,使用了compare(),其中有一個對象是cpr,這個對象是一個由K值決定類型的比較器對象;稍稍解釋一下,就是如果K值類型是Integer,那麼就可以通過cpr調用Integer類中的compare()方法

得出結論:Integer、String等對象Object,java中已經爲這些對象寫了compare();

總結:通過源碼我們可以發現,存入元素的時候,它創建了一個樹,第一個元素就是樹的根節點,後面的元素依次從樹的根節點開始向後比較(創建比較器,利用comparator()方法進行比較),小的就往左邊放,大的就往右邊放,而相同的就不放進去(實現了唯一性)。取出元素的時候,它採用中序遍歷的方法(左子樹 根節點 右子樹)遍歷整個樹,達到有序。

自定義對象的保存

那麼如果一個自定義對象要存儲到TreeSet集合中該怎麼辦呢?

假如源程序是這樣的,自定義對象爲person對象,測試一下:

class Person{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
}
​
public class testTreeSet {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        System.out.println(set);
    }
}

輸出結果:

呀,報錯了

不要着急,不要着急,休息一下,休息一下

三、TreeSet保存自定義對象時的排序分析

TreeSet保存自定義對象,有兩種方式保證有序和值唯一的特性;
法一:自定義類實現Comparable接口,覆寫compareTo()方法,比較規則可自己設定;
法二:構造自定義類的外部比較器類,覆寫compare()方法;將比較器對象傳入TreeSet中,比較規則可自己設定;

1.內部比較

自定義類自己實現了Comparable接口

Comparable接口

(1)java.lang.Comparable:內部排序接口

(2)類實現了Comparable表示此類具備可比較的性質

(3)比較方法compareTo(T o);

public int compareTo(T o) {}

返回正數:表示當前對象>目標對象

返回0:表示當前對象=目標對象

返回負數:表示當前對象<目標對象

實現內部比較的Person類如下:

class Person implements Comparable<Person>{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
​
    @Override
    public int compareTo(Person o) {
        //按照年齡排序
        //如果年齡相同,去比較姓名;不相同就返回年齡差值
        int num = this.age - o.age;
        int num1 = num == 0 ? this.name.compareTo(o.name) : num;
        return num1;
    }
}

測試一下,測試類如下:

public class testTreeSet {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        for(Person p : set){
            System.out.println("年齡爲:"p.getAge()+",姓名爲:"+p.getName());
        }
    }
}

測試結果:

2.外部比較器

從外部傳入一個該類的比較器對象,該類的比較器類實現了Comparator接口

Comparator接口

(1)java.util.Comparator:外部排序接口(策略模式

(2)類本身不具備可比較的特性,專門有一個類來比較自定義類的大小

(3)比較方法compare(T o1,T o2);

public int compare(T o1,T o2) {}

返回正數:表示當前對象>目標對象

返回0:表示當前對象=目標對象

返回負數:表示當前對象<目標對象

實現外部比較器的Person類:

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
​
class Person{
    private Integer age;
    private String name;
​
    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
}
​
/*按照年齡升序排序,實現了外部比較器Comparator接口*/
class PersonAgeSec implements Comparator<Person>{
​
    @Override
    public int compare(Person o1, Person o2) {
        if(o1.getAge() > o2.getAge()){
            return 1;
        }
        if(o1.getAge() < o2.getAge()){
            return -1;
        }
        return 0;
    }
}

測試類,嗯嘛嘛,跟上面一樣,哈哈哈:

public class testTreeSet {
    public static void main(String[] args) {
        Comparator p = new PersonAgeSec();
        Set<Person> set = new TreeSet<>(p);//將比較器對象傳入TreeSet的構造方法中
        set.add(new Person(18,"zhangsan"));
        set.add(new Person(19,"lisi"));
        set.add(new Person(20,"wangwu"));
        for(Person p : set){
            System.out.println("年齡爲:"+p.getAge()+",姓名爲:"+p.getName());
        }
    }
}

測試結果:

四、其他Set接口子類重複元素判斷

使用TreeSet子類進行數據保存的時候,重複元素的判斷依靠的是Comparable接口完成,但這並不是全部Set接口判斷重複元素的方式;在HashSet子類中,判斷重複元素的方式依靠的是Object類中的兩個方法;如下:

1.hash碼:public native int hashCode();
2.對象比較:public boolean equals(Object obj);

在Java中進行對象比較的操作:
(1)通過一個對象的唯一編碼找到一個對象的信息;
(2)編碼匹配後調用equals()方法進行內容的比較;

栗子:

package com.collection.Set;
​
import java.util.*;
​
class Person1{
    private Integer age;
    private String name;
​
    public Person1(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public String getName() {
        return name;
    }
​
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        if(o == null || this.getClass() != o.getClass()) return false;
        Person1 person1 = (Person1) o;
        return Objects.equals(name,person1.name) &&
                Objects.equals(age,person1.age);
    }
​
    @Override
    public int hashCode(){
        return Objects.hash(name,age);
    }
}
​
​
public class setTest {
    public static void main(String[] args) {
        Set<Person1> set = new HashSet<>();
        set.add(new Person1(20,"A"));
        //重複元素
        set.add(new Person1(20,"A"));
        set.add(new Person1(23,"y"));
        set.add(new Person1(21,"B"));
        for(Person1 p : set){
            System.out.println("年齡爲:"+p.getAge()+",姓名爲:"+p.getName());
        }
    }
}

輸出結果:

如果要想標識出對象的唯一性,一定需要equals()和hashCode()方法共同調用

結論:
(1)如果兩個對象equals()相等,那麼他們的hashCode必然相等;
(2)如果兩個對象hashCode相等,那麼他們的equals不一定相等,因爲可能產生hash碰撞;

對象的判斷必須兩個方法equals()、hashCode()返回值都相同才判斷爲相同;

寫的不恰當的可留言吶

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章