【jdk源碼1】TreeMap源碼學習

  這是看過的第一個jdk源碼(從立下目標以來):TreeMap。說實話斷斷續續的看了有好幾天了,我以爲我犯了一個錯誤,就像一開始說的那樣,我打算完徹底全看懂TreeMap關於紅黑樹的實現方式,後來我想了想,相對於花費這個對個人收益並非特別大,並且看的過程當中也有不少困惑,雖然我知道它每一步在作什麼,可是我還不知道爲何要這麼作,通過權衡,我決定先暫時放下TreeMap中關於對紅黑樹具體的實現,暫且記下有這回事。html

 

1、簡單理解

  最近也在看數據結構和算法,對紅黑樹也有了大體的理解,TreeMap是對紅黑樹的Java實現完美實現版本。本篇不打算討論TreeMap是怎麼實現紅黑樹的。java

1.1 使用Entry作爲基本單元

  首先在紅黑樹中既要保存數據,還要保存數據所處的位置,天然而然就想到用一個對象來包裝存儲的數據,TreeMap中是用一個靜態最終類Entry來實現這個功能的,而這個類是實現自Map的Entry接口:算法

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
   
    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
    
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

  能夠看到每個除了包含本身數據的key和value值,一樣有左、右、父節點的變量,幷包括一個默認的黑色字段,這樣,一個紅黑樹節點的全部信息就包含進去了,經過不斷擴充節點的left、right和parent來描述整個樹。編程

  另外在equals中爲了比較兩個值是否相等還新建了一個比較方法:安全

static final boolean valEquals(Object o1, Object o2) {
    return (o1==null ? o2==null : o1.equals(o2));
}

  這個比較方法避免了上層對null值的判斷,其實在Objects裏面已經有了,不過Objects出現的晚,因此說經常使用的工具方法就應該集中起來,否則每一個類都寫一遍可不是我心目中的jdk。數據結構

1.2 對transient的疑惑

  我對transient仍是有基本的瞭解的,當序列化對象的時候,被transient標記的字段就不會被序列化,所以我看到以下代碼時很驚訝:app

private transient Entry<K,V> root;
private transient int size = 0;

  我認爲根節點和容量是兩個很重要的屬性,序列化時爲何要去除呢,去除後會有錯誤的,爲了驗證個人想法我寫了個測試代碼:數據結構和算法

TreeMap<Integer, Integer> map=new TreeMap<>();
map.put(1, 1);
map.put(2, 1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
oos.writeObject(map);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
TreeMap<Integer, Integer> mapClone = (TreeMap<Integer, Integer>) ois.readObject();
ois.close();
mapClone.put(3, 1);
System.out.println(mapClone);

  並在反序列化對象put新數據的時候,打斷點進去看,發現root竟然不是null,而size也不是0,也是真實容量2,我暈了,超出了個人理解。通過測試和查找沒有解決問題,在這留個坑,但願有人能幫到我。ide

1.3 對吞異常的反感

  以前我寫過一篇博客,是講解異常的,能夠去看一下:Java異常體系簡析。因此我對把異常捕獲而不作任何處理很是反感,所以看到如下代碼,以爲很是不適:工具

public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

  什麼叫作cannotHappen,不可能發生那異常還有必要存在嗎,我仍是認爲應該拋一個虛擬機異常。不理解的能夠去看我上面說的那篇博客。

1.4 遍歷時防止另外線程的修改

  TreeMap不是一個線程安全的類,當你在遍歷一個TreeMap的時候,若是有另一個線程修改了TreeMap,會當即拋異常,是經過一個變量來實現的:

private transient int modCount = 0;

  每次修改都會讓這個數加一,而後遍歷時每次都會判斷這個數是否發生變化:

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    int expectedModCount = modCount;
    for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
        action.accept(e.key, e.value);

        if (expectedModCount != modCount) {
            throw new ConcurrentModificationException();
        }
    }
}

  這個能夠在必定程度上防止遍歷時被修改,只是被動防護,可是真要想達到隨意的增刪改查仍是要使用ConcurrentSkipListMap。

1.5 建立時沒有值的作法

  當咱們須要遍歷集合的時候,有一種作法是先獲取entrySet,而後遍歷entrySet的Entry並獲取key和value,在看到下面獲取entrySet的代碼時:

public Set<Map.Entry<K,V>> entrySet() {
    EntrySet es = entrySet;
    return (es != null) ? es : (entrySet = new EntrySet());
}

  產生了疑惑,當獲取時若爲null就去建立一個new EntrySet(),而它的構造方法就是一個空的???,也就是說返回的確實是空的一個EntrySet,可是使用它的時候確實也有值:

TreeMap<Integer,Integer> map=new TreeMap<>();
map.put(1, 1);
Set<Entry<Integer, Integer>> set = map.entrySet();
System.out.println(set);

  我在map.entrySet()這一行,debug進去一步一步看,確實是沒有值,找了好一陣子,後來在debug這System.out.println(set)的時候才發現玄機。

  簡單來講,當你得到entrySet的時候確實是沒有值的,可是它內部持有一個TreeMap的一個遍歷器,而在真正使用的時候纔去遍歷關聯的TreeMap,感受和jdk1.5的Stream同樣,所以方法的註釋也都說明是一個view。而TreeMap中大量應用了這種方式,我以爲其餘集合應該也是這麼實現的。

  這就給咱們一種編程思路,不是說全部數據在獲取的時候都須要給它準備好,等他真正使用的時候再給也不晚。

1.6 對經常使用方法的封裝

  TreeMap值有一些這樣的方法:

private static <K,V> boolean colorOf(Entry<K,V> p) {
    return (p == null ? BLACK : p.color);
}
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
    return (p == null ? null: p.parent);
}
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
    if (p != null)
        p.color = c;
}
private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
    return (p == null) ? null: p.left;
}
private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
    return (p == null) ? null: p.right;
}

  TreeMap中最經常使用的,判斷和獲取顏色,獲取左右節點等,雖然能夠經過【.】直接來得到屬性,可是有時候還必需要判斷是不是null值,因此當大量須要此處判斷的時候,封裝方法或許是咱們最好的選擇。

2、問題及總結

  這個TreeMap代碼有3000多行,雖然也有不少註釋,代碼量依然很大,不過細看起來,只針對紅黑樹實現的增刪改查並無那麼多的代碼量,而多出來的實現,只是爲了方便使用者提供了各類各樣的方法,並所以而新建了不少內部類,而對於用戶來講只是感受到了方便。方法特別多,建議看Java API。稍微總結一點:

  • TreeMap是有序Map,按照指定比較器或者key的比較性
  • TreeMap不是線程安全的

2.1 遺留問題

  看到如今還遺留兩個問題:

2.1.1 transient沒有生效

  這個我隨後會深刻測試研究。

2.1.2 紅黑樹的具體實現方式

  這個我暫時不去研究,等之後須要本身實現的時候,再來找它。

相關文章
相關標籤/搜索