此篇用以致敬:那些年,我們一起學過的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()返回值都相同才判斷爲相同;
寫的不恰當的可留言吶