Java 集合類實現原理

Collection & Map

  1. Collection 子類有 ListSet

  2. List –> ArrayList / LinkedList / Vector

  3. Set –> HashSet / TreeSet

  4. Map –> HashMap / HashTable / TreeMap

一、ArrayList

ArrayList 是 List 接口的可變數組的實現。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 接口外, 此類還提供一些方法來操作內部用來存儲列表的數組的大小。

每個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它總是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增長 (每次調用添加操作時,都會調用 ensureCapacity 方法,判斷是否需要自增,如果需要則自增數組) 。自動增長會帶來數據向新數組的重新拷貝,因此,如果可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可以使用 ensureCapacity 操作來增加 ArrayList 實例的容量,這可以減少遞增式再分配的數量。

注意,此實現不是同步的。如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。(結構上的修改是指任何添加或刪除一個或多個元素的操作,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改。)

不管是 ArrayList、 Vector、LinkedList 他們的 set,remove 方法的返回值都是原來該位置的元素,add 方法返回 boolean 值爲是否成功插入

1、實現的接口

  1. 繼承 AbstractList (實現了 List 接口)

  2. Cloneable 可克隆, Serializable 可序列化,RandomAccess 爲 List 提供快速訪問功能(RandomAccess 爲空接口,只是一個可以快速訪問的標識),即通過序號獲取元素

2、構造方法

  1. 創建長度爲 10 的數組

  2. 創建指定長度數組,小於 0 拋出異常

  3. 根據集合創建數組,創建長度爲集合長度的數組並拷貝

3、增刪查方法

  1. 每次操作之前都會創建一個新的數組引用指向被操作數組,使用新的引用操作。

  2. set 方法,指定位置賦值,檢查 index ,如果不合法則拋出異常

  3. add 方法,末尾位置添加,如果超出,先創建新數組替換舊數組,新數組長度爲就數組的 1.5 倍再加 1;

  4. add(int index,Object obj) 指定位置添加,檢查 index ,如不合法則拋出異常。指定位置插入時,會將原來的數組以 index 爲界,將 index 後的數據後移一位,後移的實現通過 System.arraycopy 方法實現。再在 index 位置插入需要插入的數據。 System.arraycopy 爲 Native 層的方法,可以高效複製數組元素。

  5. remove(int index) 根據索引刪除,直接操作數組,返回值爲被移除的對象。將該對象所在位置之後的數組內容複製到從該位置開始,將末尾置爲 null

  6. remove(Object obj) 根據對象刪除,遍歷數組,如果存在,將該對象所在位置之後的數組內容複製到從該位置開始,將末尾置爲 null

  7. 當我們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用ensureCapacity 方法來手動增加 ArrayList 實例的容量。

  8. ArrayList基於數組實現,可以通過下標索引直接查找到指定位置的元素,因此查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。

  9. 在查找給定元素索引值等的方法中,源碼都將該元素的值分爲null和不爲null兩種情況處理,ArrayList中允許元素爲null。

二、Vector

  1. Vector也是基於數組實現的,是一個動態數組,其容量能自動增長。
  2. Vector是JDK1.0引入了,它的很多實現方法都加入了同步語句,使用 synchronized 修飾,因此是線程安全的(其實也只是相對安全,有些時候還是要加入同步語句來保證線程的安全),可以用於多線程環境。
  3. Vector沒有實現 Serializable 接口,因此它不支持序列化,實現了 RandomAccess 可以
  4. Vector 的構造函數中可以指定容量增長係數,如果不指定增長係數,增加時爲增加一倍,這點有別於 ArrayList。

Vector的源碼實現總體與ArrayList類似,關於Vector的源碼,給出如下幾點總結:

1、Vector有四個不同的構造方法。無參構造方法的容量爲默認值10,僅包含容量的構造方法則將容量增長量(從源碼中可以看出容量增長量的作用,第二點也會對容量增長量詳細說)明置爲0。

2、注意擴充容量的方法ensureCapacityHelper。與ArrayList相同,Vector在每次增加元素(可能是1個,也可能是一組)時,都要調用該方法來確保足夠的容量。當容量不足以容納當前的元素個數時,就先看構造方法中傳入的容量增長量參數CapacityIncrement是否爲0,如果不爲0,就設置新的容量爲就容量加上容量增長量,如果爲0,就設置新的容量爲舊的容量的2倍,如果設置後的新容量還不夠,則直接新容量設置爲傳入的參數(也就是所需的容量),而後同樣用Arrays.copyof()方法將元素拷貝到新的數組。

3、很多方法都加入了synchronized同步語句,來保證線程安全。

4、同樣在查找給定元素索引值等的方法中,源碼都將該元素的值分爲null和不爲null兩種情況處理,Vector中也允許元素爲null

5、其他很多地方都與ArrayList實現大同小異,Vector現在已經基本不再使用。

三、LinkedList

LinkedList 和 ArrayList 一樣,實現了 List 接口,但其內部的數據結構有本質不同。LinkedList 是基於雙向循環鏈表實現的,所以它的插入和刪除操作比 ArrayList 更高效,不過由於是基於鏈表的,隨機訪問的效率要比 ArrayList 差。

實現了 Searializable 接口,支持序列化,實現了 Cloneable 接口,可被克隆

是非線程安全的,只是用於單線程環境下,多線程環境下可以採用concurrent併發包下的concurrentHashMap。

1、數據結構

LinkedList 是基於鏈表結構的實現,每一個節點的類都包含了 previous 和 next 兩個 Link 指針對象,由 Link 保存,Link 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表,每個 Link 只能知道自己的前一個和後一個節點。
注意:不同版本類名不同,但是原理一樣,有的版本類名是 Node

private static final class Link<ET> {
    ET data;

    Link<ET> previous, next;

    Link(ET o, Link<ET> p, Link<ET> n) {
        data = o;
        previous = p;
        next = n;
    }
}

2、插入數據

  1. LinkedList 內部的 Link 對象 voidLink ,其 previous 執向鏈表最後一個對象,next 指向第一個鏈表第一個對象,初始化 LinkedList 時默認初始化 voidLink 的前後都指向自己。

  2. 注意兩個不同的構造方法。無參構造方法直接建立一個僅包含head節點的空鏈表,包含Collection的構造方法,先調用無參構造方法建立一個空鏈表,而後將Collection中的數據加入到鏈表的尾部後面。

  3. 往最後插入,會創建新的 Link 對象,並將 新對象的 previous 賦值爲 voidLind 的 previous,將新對象的 next 賦值爲 voidLink,最後將 voidLink 的 previous 指向 新對象。

  4. 往非末尾插入,會比較 index 與鏈表的中間值的大小,縮小檢索比例,調用從後往前檢索或從前往後檢索,如果從前往後,會循環調用 voidLink 的 next 方法
    直到需要插入的位置得到當前位置的元素 link (注意,voidLink的 next 指向第一個元素,所以遍歷next之後的位置爲需要插入的位置),創建新對象,新對象的 previous 指向原來當前元素 link 的 previous ,新對象的 next 指向 link,link 的 previous 執向新對象,原來 link 的 previous 對象的 next 指向 新元素,這樣就準確插入。從後往前的道理相同。

  5. LinkedList 獲取非首尾元素時,也會使用與插入時相同的判斷位置的加速機制

  6. 在查找和刪除某元素時,源碼中都劃分爲該元素爲 null 和不爲 null 兩種情況來處理,LinkedList 支持插入的元素爲 null

  7. LinkedList是基於鏈表實現的,因此不存在容量不足的問題,所以這裏沒有擴容的方法。

  8. LinkedList是基於鏈表實現的,因此插入刪除效率高,查找效率低(雖然有一個加速動作)。

  9. 要注意源碼中還實現了棧和隊列的操作方法,因此也可以作爲棧、隊列和雙端隊列來使用 push(向頂部插入元素)、pop(刪除並返回第一個元素) 等方法。

  10. Iterator 中通過元素索引是否等於“雙向鏈表大小”來判斷是否達到最後。

四、HashMap

HashMap 是基於哈希表實現的,每一個元素是一個 key-value 對,其內部通過 單鏈表 解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

HashMap是非線程安全的,只是用於單線程環境下,多線程環境下可以採用concurrent併發包下的concurrentHashMap。

HashMap 實現了Serializable接口,因此它支持序列化,實現了Cloneable接口,能被克隆

默認長度 16 擴容爲 2 倍每次,如果擴容後還是不夠則創建目標長度數組,將舊數組複製到新數組中

實現方式爲數組,每個數組中都可以是一個單鏈表,插入時,根據 hashcode 計算在數組中位置,判斷是否存在相同元素後,根據情況在相應位置的鏈表頭中插入新元素。

初始容量: 初始哈希數組的長度,默認 16
最大容量: 2 的 30 次冪
加載因子: 默認 0.75
閾值:用於判斷是否需要調整 HashMap 容量,等於 容量 * 加載因子

總結

  1. 加載因子,如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那麼表中的數據將過於稀疏(很多空間還沒用,就開始擴容了),對空間造成嚴重浪費。如果我們在構造方法中不指定,則系統默認加載因子爲0.75,這是一個比較理想的值,一般情況下我們是無需修改的。

  2. 最大容量,無論我們指定的容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的次方的一個數,且最大值不能超過2的30次方。要求爲 2 的整數次冪是爲了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。

  3. HashMap中key和value都允許爲null。

HashMap 的數據結構

HashMap 中的數組就是哈希表,也稱爲哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。

數組中的每一個元素都是一個 HashMapEntry,也是一個單鏈表的表頭,其 next 指向鏈表中下一元素。通過 HashMapEntry 中的 key 可以計算出其在數組也就是哈希數組中的位置,得到該位置之後,就可以在鏈表中根據 key 的 equals 方法確定某元素

其中還有一個成員遍歷 entryForNullKey ,表示 key 爲 null 的元素,訪問和修改 key 爲 null 的元素時,直接操作該值,之前是將 key 爲 null 的元素放到了數組的第一個位置中的鏈表中,不同版本處理不同

static class HashMapEntry<K, V> implements Entry<K, V> {
    final K key;
    V value;
    final int hash;
    HashMapEntry<K, V> next;

    HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
        this.key = key;
        this.value = value;
        this.hash = hash;
        this.next = next;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

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

    @Override public final boolean equals(Object o) {
        if (!(o instanceof Entry)) {
            return false;
        }
        Entry<?, ?> e = (Entry<?, ?>) o;
        return Objects.equal(e.getKey(), key)
                && Objects.equal(e.getValue(), value);
    }

    @Override public final int hashCode() {
        return (key == null ? 0 : key.hashCode()) ^
                (value == null ? 0 : value.hashCode());
    }

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

插入 put(K key, V value)

@Override public V put(K key, V value) {
    if (key == null) {
        return putValueForNullKey(value);
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // No entry for (non-null) key is present; create one
    modCount++;
    if (size++ > threshold) {
        tab = doubleCapacity(); // 每次加入鍵值對時,都要判斷當前已用的槽的數目是否大於等於閥值(容量*加載因子),如果大於等於,則進行擴容,將容量擴爲原來容量的2倍。
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);
    return null;
}

void addNewEntry(K key, V value, int hash, int index) {
    table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
  1. 如果 key 爲 null 且存在 key 爲 null 的元素,如果沒有則在數組中添加一個 key 爲 null 的元素,如果有 key 爲 null 的元素,則將該元素對應的 value 設置爲新的 value

  2. key 不爲 null 情況下,由 key 計算 hash 值,由 hash 值確定該元素在哈希數組中的位置

  3. 如果當前哈希數組該位置中有值,且在當前鏈表中有 key 與新元素的 key 相同的元素(hash 值一樣,equals 也一樣) 說明是修改,則將舊元素的 value 更新爲新的 value ,並將舊的 value 返回,put 方法結束

  4. 如果當前哈希數組該元素爲 null 或者當前位置的鏈表中沒有 key 與新元素 key 相同的元素,那麼就是插入操作。HashMap 中元素總數量加一,執行插入方法

  5. 插入時,構造新的 HashEntry ,如果當前位置爲 null 就直接放入數組,如果該位置不爲 null ,就講新 HashEntry 的 next 指向當前位置的 HashEntry ,並將數組當前位置賦值爲新的 HashEntry,插入結束。插入成功返回值爲 null。說明每次put鍵值對的時候,總是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。

刪除 remove(Object key)

  1. 如果 key 爲 null 則直接將 key 爲 null 位置置爲 null,並將其 value 返回

  2. 如果 key 不爲 null,則根據 key 計算 hash 值再計算在哈希數組中的位置

  3. 如果當前位置 HashEntry 不爲 null,則遍歷單鏈表,找到元素的 key 與要刪除的 key 相同的元素,將其上一位置的 next 指向其 next,將該元素從鏈表中移除並返回

  4. 如果當前位置爲 null,或者遍歷完鏈表沒有 key 匹配的元素,直接返回 null

@Override public V remove(Object key) {
    if (key == null) {
        return removeNullKey();
    }
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index], prev = null;
            e != null; prev = e, e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            if (prev == null) {
                tab[index] = e.next;
            } else {
                prev.next = e.next;
            }
            modCount++;
            size--;
            postRemove(e);
            return e.value;
        }
    }
    return null;
}

查詢 get(Object key)

public V get(Object key) {
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        return e == null ? null : e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            return e.value;
        }
    }
    return null;
}

由插入和刪除的分析,查詢就比較簡單了,不再分析

四、HashTable

  1. Hashtable同樣是基於哈希表實現的,同樣每個元素是一個key-value對,其內部也是通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

  2. Hashtable也是JDK1.0引入的類,是線程安全的,能用於多線程環境中。

  3. Hashtable同樣實現了Serializable接口,它支持序列化,實現了Cloneable接口,能被克隆。

針對Hashtable,我們同樣給出幾點比較重要的總結,但要結合與HashMap的比較來總結

  1. 二者的存儲結構和解決衝突的方法都是相同的。

  2. HashTable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪

  3. Hashtable中key和value都不允許爲null,而HashMap中key和value都允許爲null(key只能有一個爲null,而value則可以有多個爲null)。但是如果在Hashtable中有類似put(null,null)的操作,編譯同樣可以通過,因爲key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。

  4. Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍

  5. Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算。取模運算開銷比較大

五、LinkedHashMap

LinkedHashMap 是 HashMap 的子類,是有序的,放入順序和訪問順序兩種初始化方式

LinkedHashMap 可以用來實現LRU算法

LinkedHashMap 同樣是非線程安全的,只在單線程環境下使用

LinkedHashMap 中有個 boolean accessOrder 成員變量,表示雙向鏈表中元素排序規則的標誌位。accessOrder爲false,表示按插入順序排序,accessOrder爲true,表示按訪問順序排序

數據結構

實際上就是 HashMap 和 LinkedList 兩個集合類的存儲結構的結合。在 LinkedHashMapMap 中,所有 put 進來的 Entry 都保存在哈希表中,但它又額外定義了一個 head 爲頭結點的空的雙向循環鏈表,每次 put 進來 HashMapEntry ,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鏈表的尾部。

static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
    LinkedEntry<K, V> nxt;
    LinkedEntry<K, V> prv;

    /** Create the header entry */
    LinkedEntry() {
        super(null, null, 0, null);
        nxt = prv = this;
    }

    /** Create a normal entry */
    LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
        super(key, value, hash, next);
        this.nxt = nxt;
        this.prv = prv;
    }
}

LinkedHashMap 中元素的類型爲 LinkedEntry,其繼承了 HashMapEntry,並添加了 nxt、prv 兩個成員,指向該元素在雙鏈表中的前後節點

put 方法

LinkedHashMap 並沒有重寫 put 方法,而是重寫了 HashMap 中添加元素時調用的 preModify 方法和 addNewEntry 方法,proModify 在 HashMap 爲空實現,在 LinkedHashMap 中調用了 makeTail 方法,接着來看:

preModify 方法是加入元素時判斷有對應 key 元素存在的情況執行的方法,說明原來的哈希數組中跟雙向鏈表中有該元素,在哈希數組中會直接修改該元素的 value 值,但是雙鏈表中的處理確有不同。此時會先將鏈表中該處的元素移除,再重新將元素的 value 賦值,之後重新將元素接到鏈表的尾端。

// 先將鏈表中該處的元素移除,再重新將元素的 value 賦值,之後重新將元素接到鏈表的尾端
private void makeTail(LinkedEntry<K, V> e) {
    // Unlink e
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;

    // Relink e as tail
    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    modCount++;
}

// 如果原鏈表中沒有匹配的 key 對應的元素,則直接將新元素添加到尾端
// 並將新元素的 next 執行原來哈希數組中該位置元素,併爲哈希數組中對應位置賦值爲新元素
@Override void addNewEntry(K key, V value, int hash, int index) {
    LinkedEntry<K, V> header = this.header;

    // Remove eldest entry if instructed to do so.
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) { // removeEldestEntry 默認返回 false,作用最後再說
        remove(eldest.key);
    }

    // Create new entry, link it on to list, and put it into table
    LinkedEntry<K, V> oldTail = header.prv;
    LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
            key, value, hash, table[index], header, oldTail);
    table[index] = oldTail.nxt = header.prv = newTail;
}
  1. 如果當前雙鏈表中有該元素,則將該元素移出鏈表,再重新將該元素接到雙鏈表尾端
  2. 如果雙鏈表中沒有對應元素,則創建一個新的 LinkedEntry ,其前一節點指向鏈表末端,其下節點指向 header,將其插入到鏈表末端,並將原來末端的下一節點指向新元素,將 header 的前一節點指向新元素,並將新元素的 next 執行原來哈希數組中該位置元素,併爲哈希數組中對應位置賦值爲新元素

get(Object key)

由 HashMap 中遍歷數組,改爲了遍歷雙鏈表,效率更高。

需要注意的是如果 LinkedHashMap 的 accessOrder 爲 true 時,會將需要獲取的元素移出雙鏈表,並重新連接到鏈表的尾端。

@Override public V get(Object key) {
    /*
     * This method is overridden to eliminate the need for a polymorphic
     * invocation in superclass at the expense of code duplication.
     */
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null)
            return null;
        if (accessOrder)
            makeTail((LinkedEntry<K, V>) e);
        return e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

remove(Object key)

LinkedHashMap 重寫了 HashMap 的 postRemove 方法,HashMap 的 remove 方法中會將哈希數組中的元素移除,同時還會調用 postRemove 方法,該方法在 HashMap 中是空實現,在 LinkedHashMap 中實現如下:

@Override void postRemove(HashMapEntry<K, V> e) {
    LinkedEntry<K, V> le = (LinkedEntry<K, V>) e;
    le.prv.nxt = le.nxt;
    le.nxt.prv = le.prv;
    le.nxt = le.prv = null; // Help the GC (for performance)
}

postRemove 方法中,會將對應元素在雙鏈表中刪除

LinkedHashMap 實現 LRU 算法

剛纔分析 addNewEntry 時提到了 removeEldestEntry 方法,其在 LinkedHashMap 中是個空實現


@Override void addNewEntry(K key, V value, int hash, int index) {
    ...
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
        remove(eldest.key);
    }
    ...
}

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return false;
}

在每次添加元素是都會調用 removeEldestEntry 方法,如果該方法返回 true 則刪除 header.nxt 處的元素,其實也就是刪除哈希數組中對應元素與雙向鏈表的頭部的元素,因爲在 accessOrder 爲 true 時每次插入和訪問都會將最近訪問的元素移動到雙向鏈表的尾部,這樣鏈表頭部的元素就是最久沒有被訪問到的。在 removeEldestEntry 中可以根據當前鏈表節點數到達最大容量時返回 true,此時就會刪除鏈表頭部節點,這樣就完成了 LRU 算法。

總結

  1. 實際上就是HashMap和LinkedList兩個集合類的存儲結構的結合。在LinkedHashMapMap中,所有put進來的Entry都保存在如第一個圖所示的哈希表中,但它又額外定義了一個以head爲頭結點的空的雙向循環鏈表,每次put進來Entry,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鏈表的尾部。

2、LinkedHashMap由於繼承自HashMap,因此它具有HashMap的所有特性,同樣允許key和value爲null。

  1. 注意構造方法,前四個構造方法都將accessOrder設爲false,說明默認是按照插入順序排序的,而第五個構造方法可以自定義傳入的accessOrder的值,因此可以指定雙向循環鏈表中元素的排序規則,一般要用LinkedHashMap實現LRU算法,就要用該構造方法,將accessOrder置爲true。

  2. 最後說說LinkedHashMap是如何實現LRU的。首先,當accessOrder爲true時,纔會開啓按訪問順序排序的模式,才能用來實現LRU算法。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成爲最近訪問的Entry,因此便把該Entry加入到了雙向鏈表的末尾(get方法通過調用recordAccess方法來實現,put方法在覆蓋已有key的情況下,也是通過調用recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的後面,多次操作後,雙向鏈表前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head後面的那個Entry)便是最近最少使用的Entry。

六、TreeMap

TreeMap是基於紅黑樹實現的,這裏只對紅黑樹做個簡單的介紹,紅黑樹是一種特殊的二叉排序樹,紅黑樹通過一些限制,使其不會出現二叉樹排序樹中極端的一邊倒的情況,相對二叉排序樹而言,這自然提高了查詢的效率。

紅黑樹規則

  1. 每個節點都只能是紅色或者黑色
  2. 根節點是黑色
  3. 每個葉節點(NIL節點,空節點)是黑色的。
  4. 如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
  5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

正是這些性質的限制,使得紅黑樹中任一節點到其子孫葉子節點的最長路徑不會長於最短路徑的2倍,因此它是一種接近平衡的二叉樹。

構造方法

  1. 採用無參構造方法,不指定比較器,這時候,排序的實現要依賴key.compareTo()方法,因此key必須實現Comparable接口,並覆寫其中的compareTo方法。

  2. 採用帶比較器的構造方法,這時候,排序依賴該比較器,key可以不用實現Comparable接口。

  3. 帶Map的構造方法,該構造方法同樣不指定比較器,調用putAll方法將Map中的所有元素加入到TreeMap中。putAll的源碼如下:

  4. TreeMap是根據key進行排序的,它的排序和定位需要依賴比較器或覆寫Comparable接口,也因此不需要key覆寫hashCode方法和equals方法,就可以排除掉重複的key,而HashMap的key則需要通過覆寫hashCode方法和equals方法來確保沒有重複的key。

  5. TreeMap的查詢、插入、刪除效率均沒有HashMap高,一般只有要對key排序時才使用TreeMap。

  6. TreeMap的key不能爲null,而HashMap的key可以爲null。

七、HashSet

實現原理基於 HashMap set 中的 value 都一樣,key 爲添加的元素

HashSet 中有一個 HashMap 成員,每次操作時都是操作該 HashMap,操作的元素的 key 爲 Set 中要操作的元素,value 爲 HashSet 本身的引用。

八、TreeSet

實現原理基於 TreeMap

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