【傳智播客上海校區】TreeMap原始碼解析

胖影發表於2018-04-16

案例剖析

關於Map集合的特點,我們都知道底層資料結構是控制Map集合的鍵,而和值即value是無關的。通過查閱JDK的API我們發現TreeMap集合的底層是基於紅黑樹的即二叉樹。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的Comparator進行排序,具體取決於使用的構造方法。如果使用的構造方法是TreeMap()那麼底層的鍵就是自然排序,如果使用的構造方法是TreeMap(Comparator<? super K> comparator) ,那麼底層的鍵就是Comparator進行排序的。

接下來我們先看如下案例:

向TreeMap集合中儲存資料,要求鍵為自定義Student類的物件,家庭住址作為TreeMap的值,型別是String型別。

學生Student類程式碼如下:

packagecn.itcast.sh.set;/** 描述學生*/publicclassStudent {//屬性Stringname;intage;publicStudent(Stringname, intage) {this.name=name;this.age=age;}@OverridepublicStringtoString() {return"Student [name="+name+", age="+age+"]";}}

測試類TreeMapDemo01程式碼如下:

publicclassTreeMapDemo01 {publicstaticvoidmain(String[] args) {// 建立集合物件TreeMap<Student, String>tm=newTreeMap<Student,String>();//新增資料tm.put(newStudent("張三",18), "上海");tm.put(newStudent("李四",19), "北京");tm.put(newStudent("王五",18), "深圳");tm.put(newStudent("張三",18), "上海");//獲取所有的鍵Set<Student>keys=tm.keySet();//遍歷Set集合for (Studentkey : keys) {//輸出資料System.out.println(key+"----"+tm.get(key));}}}

上述程式碼執行結果會報如下錯誤:

分析異常的原因:

通過以上報異常錯誤原因大概知道我們要將自己定義的類Student的儲存到TreeMap集合中的時候,發生自定義類Student不能被轉換到Comparable比較器介面的異常,我們在程式碼中明明沒有書寫和Comparable介面相關的程式碼啊,那怎麼會報這個異常呢?

這裡我們需要檢視一下TreeMap底層的原始碼進行進一步分析。

關於TreeMap集合的深入分析

問題1:為什麼集合中儲存的元素可以不重複呢?

TreeMap集合底層使用了二叉樹結構,在儲存元素時,拿儲存的元素會和樹結構中已經存在元素進行比較大小(結果有三種:大於、小於、相等)。當比較的結果相等時,表示該元素已經存在了,則不儲存。

問題2:為什麼TreeMap集合中儲存的元素會排序呢?

在儲存元素時,先拿要儲存的元素和樹結構中已經存在的元素進行比較大小,如果要儲存的元素大於樹結構中已存在的元素,則把要儲存的元素存放比較元素的右邊;如果要要儲存的元素小於樹結構中已存在的元素,則把要儲存的元素放在左邊。

分析TreeMap集合的原始碼查詢為什麼會報上述異常:

當上述程式碼中我們使用集合物件tm呼叫put方法向集合新增資料時,報了異常。所以我們可以檢視put方法的原始碼:

TreeMap集合中的put方法原始碼如下所示:

publicVput(Kkey, Vvalue) {Entry<K,V>t=root;//表示二叉樹的根元素if (t==null) {compare(key, key); // type (and possibly null) checkroot=newEntry<>(key, value, null);size=1;modCount++;returnnull; }intcmp;//記錄比較的結果,有三種結果 大 小 相等Entry<K,V>parent;// split comparator and comparable pathsComparator<?superK>cpr=comparator;//自定義比較器物件if (cpr!=null) {//判斷自定義比較器物件是否為null,由於我們在建立TreeMap集合的時候根本就沒有傳遞自定義比較器物件,所以cpr是null,那麼這裡false,所以不會執行if中的程式碼,執行elsedo {parent=t;cmp=cpr.compare(key, t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }else {if (key==null)thrownewNullPointerException();Comparable<?superK>k= (Comparable<?superK>) key;do {parent=t;cmp=k.compareTo(t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }Entry<K,V>e=newEntry<>(key, value, parent);if (cmp<0)parent.left=e;elseparent.right=e;fixAfterInsertion(e);size++;modCount++;returnnull; }

對於上述原始碼的解釋:

Entry<K,V> t = root;//表示二叉樹的根元素

if (t==null) {//判斷根元素t是否為空,在插入第一個資料之前根元素肯定是nullcompare(key, key); // type (and possibly null) checkroot=newEntry<>(key, value, null);size=1;modCount++;returnnull; }compare(key, key);比較key,其實這裡是判斷key位置物件所屬類即Student是否實現Comparable介面。程式碼如下:```javafinal int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); }

由於我們在建立TreeMap物件時並沒有傳遞自定義比較器Comparator的物件,所以comparator等於null。這裡會執行 ((Comparable<? super K>)k1).compareTo((K)k2)。這句話的意思是對兩個鍵進行比較,按照自然排序Comparable介面中的compareTo進行排序,但是必須將傳遞進來的Student物件進行強轉。但是Student根本就沒有實現Comparable介面,所以這裡會報類轉換異常。為了解決上述錯誤,只需Student實現Comparable介面即可。

root = new Entry<>(key, value, null); 表示建立根元素物件,即將第一個元素新增到二叉樹的根元素位置。

由於在建立TreeMap集合物件時並沒有傳遞自定義比較器Comparator的物件,所以自定義比較器物件是null,執行如下程式碼:

else {if (key==null)//在TreeMap集合中key不能是null,只要是null就會報空指標異常thrownewNullPointerException();Comparable<?superK>k= (Comparable<?superK>) key;//將新增到集合中的key強轉為Comparable型別do {parent=t;cmp=k.compareTo(t.key);if (cmp<0)t=t.left;elseif (cmp>0)t=t.right;elsereturnt.setValue(value); } while (t!=null); }

cmp = k.compareTo(t.key);

t.key表示根元素的鍵,k表示後新增的鍵,按照自然排序進行比較。將返回值放到cmp變數中。

如果cmp變數小於0,將執行t = t.left;如果cmp變數大於0,將執行 t = t.right;如果cmp等於0,後新增的值將之前的值覆蓋。

這裡由於 t.left或者t.right初始化值都是null,所以t等於null。

執行如下程式碼:

Entry<K,V>e=newEntry<>(key, value, parent);if (cmp<0)parent.left=e;//將元素放到左子樹elseparent.right=e;//將元素放到右子樹fixAfterInsertion(e);//進行反轉size++;modCount++;returnnull;


相關文章