Java 容器源碼分析之Map-Set-List

HashMap 的實現原理

HashMap 概述

HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。java

此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高或將加載因子設置得過低。也許你們開始對這段話有一點不太懂,不過不用擔憂,當你讀完這篇文章後,就能深切理解這其中的含義了。node

須要注意的是:Hashmap 不是同步的,若是多個線程同時訪問一個 HashMap,而其中至少一個線程從結構上(指添加或者刪除一個或多個映射關係的任何操做)修改了,則必須保持外部同步,以防止對映射進行意外的非同步訪問。git

HashMap 的數據結構

在 Java 編程語言中,最基本的結構就是兩種,一個是數組,另一個是指針(引用),HashMap 就是經過這兩個數據結構進行實現。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。github

圖1

從上圖中能夠看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。面試

咱們經過 JDK 中的 HashMap 源碼進行一些學習,首先看一下構造函數:算法

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
}

咱們着重看一下第 18 行代碼table = new Entry[capacity];。這不就是 Java 中數組的建立方式嗎?也就是說在構造函數中,其建立了一個 Entry 的數組,其大小爲 capacity(目前咱們還不須要太瞭解該變量含義),那麼 Entry 又是什麼結構呢?看一下源碼:編程

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ……
}

咱們目前仍是隻着重核心的部分,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指針。咱們能夠總結出:Entry 就是數組中的元素,每一個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。數組

HashMap 的核心方法解讀

存儲

public V put(K key, V value) {
        //其容許存放null的key和null的value,當其key爲null時,調用putForNullKey方法,放入到table[0]的這個位置
        if (key == null)
            return putForNullKey(value);
        //經過調用hash方法對key進行哈希,獲得哈希以後的數值。該方法實現能夠經過看源碼,其目的是爲了儘量的讓鍵值對能夠分不到不一樣的桶中
        int hash = hash(key);
        //根據上一步驟中求出的hash獲得在數組中是索引i
        int i = indexFor(hash, table.length);
        //若是i處的Entry不爲null,則經過其next指針不斷遍歷e元素的下一個元素。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}
put

咱們看一下方法的標準註釋:在註釋中首先提到了,當咱們 put 的時候,若是 key 存在了,那麼新的 value 會代替舊的 value,而且若是 key 存在的狀況下,該方法返回的是舊的 value,若是 key 不存在,那麼返回 null。緩存

從上面的源代碼中能夠看出:當咱們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 從新計算 hash 值,根據 hash 值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。安全

addEntry(hash, key, value, i)方法根據計算出的 hash 值,將 key-value 對放在數組 table 的 i 索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼以下:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
        // 獲取指定 bucketIndex 索引處的 Entry
        Entry<K,V> e = table[bucketIndex];
        // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。

hash(int h)方法根據 key 的 hashCode 從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的 hash 衝突。

final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
        //獲得k的hashcode值
        h ^= k.hashCode();
        //進行計算
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

咱們能夠看到在 HashMap 中要找到某個元素,須要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。前面說過 HashMap 的數據結構是數組和鏈表的結合,因此咱們固然但願這個 HashMap 裏面的 元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用 hash 算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。

對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 hash 碼值老是相同的。咱們首先想到的就是把 hash 值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,「模」運算的消耗仍是比較大的,在 HashMap 中是這樣作的:調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:

static int indexFor(int h, int length) {  
    return h & (length-1);
}

這個方法很是巧妙,它經過 h & (table.length -1) 來獲得該對象的保存位,而 HashMap 底層數組的長度老是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有以下代碼:

// Find a power of 2 >= initialCapacity
int capacity = 1;
    while (capacity < initialCapacity)  
        capacity <<= 1;

這段代碼保證初始化時 HashMap 的容量老是 2 的 n 次方,即底層數組的長度老是爲 2 的 n 次方。

當 length 老是 2 的 n 次方時,h& (length-1)運算等價於對 length 取模,也就是 h%length,可是 & 比 % 具備更高的效率。這看上去很簡單,其實比較有玄機的,咱們舉個例子來講明:

假設數組長度分別爲 15 和 16,優化後的 hash 碼分別爲 8 和 9,那麼 & 運算後的結果以下:

h & (table.length-1) hash   table.length-1  
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101

從上面的例子中能夠看出:當它們和 15-1(1110)「與」的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8 和 9 會被放到數組中的同一個位置上造成鏈表,那麼查詢的時候就須要遍歷這個鏈 表,獲得8或者9,這樣就下降了查詢的效率。同時,咱們也能夠發現,當數組長度爲 15 的時候,hash 值會與 15-1(1110)進行「與」,那麼最後一位永遠是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!而當數組長度爲16時,即爲2的n次方時,2n-1 獲得的二進制數的每一個位上的值都爲 1,這使得在低位上&時,獲得的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進一步優化,加入了高位計算,就使得只有相同的 hash 值的兩個值纔會被放到數組中的同一個位置上造成鏈表。

因此說,當數組長度爲 2 的 n 次冪的時候,不一樣的 key 算得得 index 相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

根據上面 put 方法的源代碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

讀取

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

有了上面存儲時的 hash 算法做爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中能夠看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,而後經過 key 的 equals 方法在對應位置的鏈表中找到須要的元素。

概括

簡單地說,HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當須要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。

HashMap 的 resize(rehash)

當 HashMap 中的元素愈來愈多的時候,hash 衝突的概率也就愈來愈高,由於數組的長度是固定的。因此爲了提升查詢的效率,就要對 HashMap 的數組進行擴容,數組擴容這個操做也會出如今 ArrayList 中,這是一個經常使用的操做,而在 HashMap 數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是 resize。

那麼 HashMap 何時進行擴容呢?當 HashMap 中的元素個數超過數組大小 *loadFactor時,就會進行數組擴容,loadFactor的默認值爲 0.75,這是一個折中的取值。也就是說,默認狀況下,數組大小爲 16,那麼當 HashMap 中元素個數超過 16*0.75=12 的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知 HashMap 中元素的個數,那麼預設元素的個數可以有效的提升 HashMap 的性能。

HashMap 的性能參數

HashMap 包含以下幾個構造器:

  • HashMap():構建一個初始容量爲 16,負載因子爲 0.75 的 HashMap。
  • HashMap(int initialCapacity):構建一個初始容量爲 initialCapacity,負載因子爲 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。

HashMap 的基礎構造器 HashMap(int initialCapacity, float loadFactor) 帶有兩個參數,它們是初始容量 initialCapacity 和負載因子 loadFactor。

負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來講,查找一個元素的平均時間是 O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。

HashMap 的實現中,經過 threshold 字段來判斷 HashMap 的最大容量:

threshold = (int)(capacity * loadFactor);

結合負載因子的定義公式可知,threshold 就是在此 loadFactor 和 capacity 對應下容許的最大元素數目,超過這個數目就從新 resize,以下降實際的負載因子。默認的的負載因子 0.75 是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize 後的 HashMap 容量是容量的兩倍:

Fail-Fast 機制

原理

咱們知道 java.util.HashMap 不是線程安全的,所以若是在使用迭代器的過程當中有其餘線程修改了 map,那麼將拋出 ConcurrentModificationException,這就是所謂 fail-fast 策略。

ail-fast 機制是 java 集合(Collection)中的一種錯誤機制。 當多個線程對同一個集合的內容進行操做時,就可能會產生 fail-fast 事件。

例如:當某一個線程 A 經過 iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事件。

這一策略在源碼中的實現是經過 modCount 域,modCount 顧名思義就是修改次數,對 HashMap 內容(固然不只僅是 HashMap 纔會有,其餘例如 ArrayList 也會)的修改都將增長這個值(你們能夠再回頭看一下其源碼,在不少操做中都有 modCount++ 這句),那麼在迭代器初始化過程當中會將這個值賦給迭代器的 expectedModCount。

HashIterator() {
    expectedModCount = modCount;
    if (size > 0) { // advance to first entry
    Entry[] t = table;
    while (index < t.length && (next = t[index++]) == null)  
        ;
    }
}

在迭代過程當中,判斷 modCount 跟 expectedModCount 是否相等,若是不相等就表示已經有其餘線程修改了 Map:

注意到 modCount 聲明爲 volatile,保證線程之間修改的可見性。

final Entry<K,V> nextEntry() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

在 HashMap 的 API 中指出:

由全部 HashMap 類的「collection 視圖方法」所返回的迭代器都是快速失敗的:在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自己的 remove 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。所以,面對併發的修改,迭代器很快就會徹底失敗,而不冒在未來不肯定的時間發生任意不肯定行爲的風險。

注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤。

解決方案

在上文中也提到,fail-fast 機制,是一種錯誤檢測機制。它只能被用來檢測錯誤,由於 JDK 並不保證 fail-fast 機制必定會發生。若在多線程環境下使用 fail-fast 機制的集合,建議使用「java.util.concurrent 包下的類」去取代「java.util 包下的類」。

HashMap 的兩種遍歷方式

第一種 

 Map map = new HashMap();
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();
  }

效率高,之後必定要使用此種方式!

第二種

 Map map = new HashMap();
  Iterator iter = map.keySet().iterator();
  while (iter.hasNext()) {
  Object key = iter.next();
  Object val = map.get(key);
  }

效率低,之後儘可能少使用!

HashSet 的實現原理

HashSet 概述

對於 HashSet 而言,它是基於 HashMap 實現的,底層採用 HashMap 來保存元素,因此若是對 HashMap 比較熟悉了,那麼學習 HashSet 也是很輕鬆的。

咱們先經過 HashSet 最簡單的構造函數和幾個成員變量來看一下,證實我們上邊說的,其底層是 HashMap:

 private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

其實在英文註釋中已經說的比較明確了。首先有一個HashMap的成員變量,咱們在 HashSet 的構造函數中將其初始化,默認狀況下采用的是 initial capacity爲16,load factor 爲 0.75。

HashSet 的實現

對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存全部元素,所以 HashSet 的實現比較簡單,相關 HashSet 的操做,基本上都是直接調用底層 HashMap 的相關方法來完成,咱們應該爲保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()

構造方法

/**
 * 默認的無參構造器,構造一個空的HashSet。
 *
 * 實際底層會初始化一個空的HashMap,並使用默認初始容量爲16和加載因子0.75。
 */
public HashSet() {
    map = new HashMap<E,Object>();
}

/**
 * 構造一個包含指定collection中的元素的新set。
 *
 * 實際底層使用默認的加載因子0.75和足以包含指定collection中全部元素的初始容量來建立一個HashMap。
 * @param c 其中的元素將存放在此set中的collection。
 */
public HashSet(Collection<? extends E> c) {
    map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

/**
 * 以指定的initialCapacity和loadFactor構造一個空的HashSet。
 *
 * 實際底層以相應的參數構造一個空的HashMap。
 * @param initialCapacity 初始容量。
 * @param loadFactor 加載因子。
 */
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<E,Object>(initialCapacity, loadFactor);
}

/**
 * 以指定的initialCapacity構造一個空的HashSet。
 *
 * 實際底層以相應的參數及加載因子loadFactor爲0.75構造一個空的HashMap。
 * @param initialCapacity 初始容量。
 */
public HashSet(int initialCapacity) {
    map = new HashMap<E,Object>(initialCapacity);
}

/**
 * 以指定的initialCapacity和loadFactor構造一個新的空連接哈希集合。此構造函數爲包訪問權限,不對外公開,
 * 實際只是是對LinkedHashSet的支持。
 *
 * 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
 * @param initialCapacity 初始容量。
 * @param loadFactor 加載因子。
 * @param dummy 標記。
 */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

 

add 方法

/**
 * @param e 將添加到此set中的元素。
 * @return 若是此set還沒有包含指定元素,則返回true。
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

 

若是此 set 中還沒有包含指定元素,則添加指定元素。更確切地講,若是此 set 沒有包含知足(e==null ? e2==null : e.equals(e2)) 的元素 e2,則向此 set 添加指定的元素 e。若是此 set 已包含該元素,則該調用不更改 set 並返回 false。但底層實際將將該元素做爲 key 放入 HashMap。思考一下爲何?

因爲 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,經過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT),但 key 不會有任何改變,所以若是向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就知足了 Set 中元素不重複的特性。

該方法若是添加的是在 HashSet 中不存在的,則返回 true;若是添加的元素已經存在,返回 false。其緣由在於咱們以前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重複的鍵值對的時候,會返回 null。

其他方法

    /**
     * 若是此set包含指定元素,則返回true。
     * 更確切地講,當且僅當此set包含一個知足(o==null ? e==null : o.equals(e))的e元素時,返回true。
     *
     * 底層實際調用HashMap的containsKey判斷是否包含指定key。
     * @param o 在此set中的存在已獲得測試的元素。
     * @return 若是此set包含指定元素,則返回true。
     */
    public boolean contains(Object o) {
    return map.containsKey(o);
    }
    /**
     * 若是指定元素存在於此set中,則將其移除。更確切地講,若是此set包含一個知足(o==null ? e==null : o.equals(e))的元素e,
     * 則將其移除。若是此set已包含該元素,則返回true
     *
     * 底層實際調用HashMap的remove方法刪除指定Entry。
     * @param o 若是存在於此set中則須要將其移除的對象。
     * @return 若是set包含指定元素,則返回true。
     */
    public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
    }
    /**
     * 返回此HashSet實例的淺表副本:並無複製這些元素自己。
     *
     * 底層實際調用HashMap的clone()方法,獲取HashMap的淺表副本,並設置到HashSet中。
     */
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
}
相關說明
  1. 相關 HashMap 的實現原理,請參考個人上一遍總結:HashMap的實現原理。
  2. 對於 HashSet 中保存的對象,請注意正確重寫其 equals 和 hashCode 方法,以保證放入的對象的惟一性。這兩個方法是比較重要的,但願你們在之後的開發過程當中須要注意一下。

Hashtable 的實現原理

概述

和 HashMap 同樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對。

Hashtable 在 Java 中的定義爲:

public class Hashtable<K,V>  
    extends Dictionary<K,V>  
    implements Map<K,V>, Cloneable, java.io.Serializable{}

從源碼中,咱們能夠看出,Hashtable 繼承於 Dictionary 類,實現了 Map, Cloneable, java.io.Serializable接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類,每一個鍵和值都是對象(源碼註釋爲:The Dictionary class is the abstract parent of any class, such as Hashtable, which maps keys to values. Every key and every value is an object.)。但在這一點我開始有點懷疑,由於我查看了HashMap以及TreeMap的源碼,都沒有繼承於這個類。不過當我看到註釋中的解釋也就明白了,其 Dictionary 源碼註釋是這樣的:NOTE: This class is obsolete. New implementations should implement the Map interface, rather than extending this class. 該話指出 Dictionary 這個類過期了,新的實現類應該實現Map接口。

Hashtable 源碼解讀

成員變量

Hashtable是經過"拉鍊法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。

  • table是一個 Entry[] 數組類型,而 Entry(在 HashMap 中有講解過)實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
  • count 是 Hashtable 的大小,它是 Hashtable 保存的鍵值對的數量。
  • threshold 是 Hashtable 的閾值,用於判斷是否須要調整 Hashtable 的容量。threshold 的值="容量*加載因子"。
  • loadFactor 就是加載因子。
  • modCount 是用來實現 fail-fast 機制的。

關於變量的解釋在源碼註釋中都有,最好仍是應該看英文註釋。

/**
     * The hash table data.
     */
    private transient Entry<K,V>[] table;

    /**
     * The total number of entries in the hash table.
     */
    private transient int count;

    /**
     * The table is rehashed when its size exceeds this threshold.  (The
     * value of this field is (int)(capacity * loadFactor).)
     *
     * @serial
     */
    private int threshold;

    /**
     * The load factor for the hashtable.
     *
     * @serial
     */
    private float loadFactor;

    /**
     * The number of times this Hashtable has been structurally modified
     * Structural modifications are those that change the number of entries in
     * the Hashtable or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the Hashtable fail-fast.  (See ConcurrentModificationException).
     */
    private transient int modCount = 0;

構造方法

Hashtable 一共提供了 4 個構造方法:

public Hashtable(int initialCapacity, float loadFactor): 用指定初始容量和指定加載因子構造一個新的空哈希表。useAltHashing 爲 boolean,其若是爲真,則執行另外一散列的字符串鍵,以減小因爲弱哈希計算致使的哈希衝突的發生。
public Hashtable(int initialCapacity):用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。
public Hashtable():默認構造函數,容量爲 11,加載因子爲 0.75public Hashtable(Map<? extends K, ? extends V> t):構造一個與給定的 Map 具備相同映射關係的新哈希表。
/**
     * Constructs a new, empty hashtable with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hashtable.
     * @param      loadFactor        the load factor of the hashtable.
     * @exception  IllegalArgumentException  if the initial capacity is less
     *             than zero, or if the load factor is nonpositive.
     */
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        useAltHashing = sun.misc.VM.isBooted() &&
                (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     * @exception IllegalArgumentException if the initial capacity is less
     *              than zero.
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

 

put 方法

put 方法的整個流程爲:

  1. 判斷 value 是否爲空,爲空則拋出異常;
  2. 計算 key 的 hash 值,並根據 hash 值得到 key 在 table 數組中的位置 index,若是 table[index] 元素不爲空,則進行迭代,若是遇到相同的 key,則直接替換,並返回舊 value;
  3. 不然,咱們能夠將其插入到 table[index] 位置。

我在下面的代碼中也進行了一些註釋:

public synchronized V put(K key, V value) {
        // Make sure the value is not null確保value不爲null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //確保key不在hashtable中
        //首先,經過hash方法計算key的哈希值,並計算得出index值,肯定其在table[]中的位置
        //其次,迭代index索引位置的鏈表,若是該位置處的鏈表存在相同的key,則替換value,返回舊的value
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //若是超過閥值,就進行rehash操做
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        //將值插入,返回的爲null
        Entry<K,V> e = tab[index];
        // 建立新的Entry節點,並將新的Entry插入Hashtable的index位置,並設置e爲新的Entry的下一個元素
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }

經過一個實際的例子來演示一下這個過程:

假設咱們如今Hashtable的容量爲5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置以下:

圖1

如今,咱們插入一個新的鍵值對,put(16,22),假設key=16的索引爲1.但如今索引1的位置有兩個Entry了,因此程序會對鏈表進行迭代。迭代的過程當中,發現其中有一個Entry的key和咱們要插入的鍵值對的key相同,因此如今會作的工做就是將newValue=22替換oldValue=16,而後返回oldValue=16.

圖2

而後咱們如今再插入一個,put(33,33),key=33的索引爲3,而且在鏈表中也不存在key=33的Entry,因此將該節點插入鏈表的第一個位置。

圖3

get 方法

相比較於 put 方法,get 方法則簡單不少。其過程就是首先經過 hash()方法求得 key 的哈希值,而後根據 hash 值獲得 index 索引(上述兩步所用的算法與 put 方法都相同)。而後迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。

public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }

Hashtable 遍歷方式

Hashtable 有多種遍歷方式:

//一、使用keys()
Enumeration<String> en1 = table.keys();
    while(en1.hasMoreElements()) {
    en1.nextElement();
}

//二、使用elements()
Enumeration<String> en2 = table.elements();
    while(en2.hasMoreElements()) {
    en2.nextElement();
}

//三、使用keySet()
Iterator<String> it1 = table.keySet().iterator();
    while(it1.hasNext()) {
    it1.next();
}

//四、使用entrySet()
Iterator<Entry<String, String>> it2 = table.entrySet().iterator();
    while(it2.hasNext()) {
    it2.next();
}

Hashtable 與 HashMap 的簡單比較

  1. HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減小實現此接口所需的工做。
  2. HashMap 的 key 和 value 都容許爲 null,而 Hashtable 的 key 和 value 都不容許爲 null。HashMap 遇到 key 爲 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
  3. Hashtable 方法是同步,而HashMap則不是。咱們能夠看一下源碼,Hashtable 中的幾乎全部的 public 的方法都是 synchronized 的,而有些方法也是在內部經過 synchronized 代碼塊來實現。因此有人通常都建議若是是涉及到多線程同步時採用 HashTable,沒有涉及就採用 HashMap,可是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法建立了一個線程安全的 Map 對象,並把它做爲一個封裝的對象來返回。

LinkedHashMap 的實現原理

LinkedHashMap 概述

HashMap 是無序的,HashMap 在 put 的時候是根據 key 的 hashcode 進行 hash 而後放入對應的地方。因此在按照必定順序 put 進 HashMap 中,而後遍歷出 HashMap 的順序跟 put 的順序不一樣(除非在 put 的時候 key 已經按照 hashcode 排序號了,這種概率很是小)

JAVA 在 JDK1.4 之後提供了 LinkedHashMap 來幫助咱們實現了有序的 HashMap!

LinkedHashMap 是 HashMap 的一個子類,它保留插入的順序,若是須要輸出的順序和輸入時的相同,那麼就選用 LinkedHashMap。

LinkedHashMap 是 Map 接口的哈希表和連接列表實現,具備可預知的迭代順序。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。

LinkedHashMap 實現與 HashMap 的不一樣之處在於,LinkedHashMap 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序能夠是插入順序或者是訪問順序。

注意,此實現不是同步的。若是多個線程同時訪問連接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。

根據鏈表中元素的順序能夠分爲:按插入順序的鏈表,和按訪問順序(調用 get 方法)的鏈表。默認是按插入順序排序,若是指定按訪問順序排序,那麼調用get方法後,會將此次訪問的元素移至鏈表尾部,不斷訪問能夠造成按訪問順序排序的鏈表。

小 Demo

我在最開始學習 LinkedHashMap 的時候,看到訪問順序、插入順序等等,有點暈了,隨着後續的學習才慢慢懂得其中原理,因此我會先在進行作幾個 demo 來演示一下 LinkedHashMap 的使用。看懂了其效果,而後再來研究其原理。

HashMap

看下面這個代碼:

public static void main(String[] args) {
    Map<String, String> map = new HashMap<String, String>();
    map.put("apple", "蘋果");
    map.put("watermelon", "西瓜");
    map.put("banana", "香蕉");
    map.put("peach", "桃子");

    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}

一個比較簡單的測試 HashMap 的代碼,經過控制檯的輸出,咱們能夠看到 HashMap 是沒有順序的。

banana=香蕉
apple=蘋果
peach=桃子
watermelon=西瓜

LinkedHashMap

咱們如今將 map 的實現換成 LinkedHashMap,其餘代碼不變:Map<String, String> map = new LinkedHashMap<String, String>();

看一下控制檯的輸出:

apple=蘋果
watermelon=西瓜
banana=香蕉
peach=桃子

咱們能夠看到,其輸出順序是完成按照插入順序的!也就是咱們上面所說的保留了插入的順序。咱們不是在上面還提到過其能夠按照訪問順序進行排序麼?好的,咱們仍是經過一個例子來驗證一下:

public static void main(String[] args) {
    Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
    map.put("apple", "蘋果");
    map.put("watermelon", "西瓜");
    map.put("banana", "香蕉");
    map.put("peach", "桃子");

    map.get("banana");
    map.get("apple");

    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry entry = (Map.Entry) iter.next();
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}
代碼與以前的都差很少,但咱們多了兩行代碼,而且初始化 LinkedHashMap 的時候,用的構造函數也不相同,看一下控制檯的輸出結果:
watermelon=西瓜
peach=桃子
banana=香蕉
apple=蘋果

這也就是咱們以前提到過的,LinkedHashMap 能夠選擇按照訪問順序進行排序。

LinkedHashMap 的實現

對於 LinkedHashMap 而言,它繼承與 HashMap(public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>)、底層使用哈希表與雙向鏈表來保存全部元素。其基本操做與父類 HashMap 類似,它經過重寫父類相關的方法,來實現本身的連接列表特性。下面咱們來分析 LinkedHashMap 的源代碼:

成員變量

LinkedHashMap 採用的 hash 算法和 HashMap 相同,可是它從新定義了數組中保存的元素 Entry,該 Entry 除了保存當前對象的引用外,還保存了其上一個元素 before 和下一個元素 after 的引用,從而在哈希表的基礎上又構成了雙向連接列表。看源代碼:

/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
* 若是爲true,則按照訪問順序;若是爲false,則按照插入順序。
*/
private final boolean accessOrder;
/**
* 雙向鏈表的表頭元素。
 */
private transient Entry<K,V> header;

/**
* LinkedHashMap的Entry元素。
* 繼承HashMap的Entry元素,又保存了其上一個元素before和下一個元素after的引用。
 */
private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;
    ……
}
LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,可是其增長了 before 和 after 的引用,指的是上一個元素和下一個元素的引用。

初始化

經過源代碼能夠看出,在 LinkedHashMap 的構造方法中,實際調用了父類 HashMap 的相關構造方法來構造一個底層存放的 table 數組,但額外能夠增長 accessOrder 這個參數,若是不設置,默認爲 false,表明按照插入順序進行迭代;固然能夠顯式設置爲 true,表明以訪問順序進行迭代。如:

public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

咱們已經知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能。在上述 HashMap 的構造器中,最後會調用 init() 方法,進行相關的初始化,這個方法在 HashMap 的實現中並沒有意義,只是提供給子類實現相關的初始化調用。

但在 LinkedHashMap 重寫了 init() 方法,在調用父類的構造方法完成構造後,進一步實現了對其元素 Entry 的初始化操做。

/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map.  Initializes
* the chain.
*/
@Override
void init() {
  header = new Entry<>(-1, null, null, null);
  header.before = header.after = header;
}

存儲

LinkedHashMap 並未重寫父類 HashMap 的 put 方法,而是重寫了父類 HashMap 的 put 方法調用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了本身特有的雙向連接列表的實現。咱們在以前的文章中已經講解了HashMap的put方法,咱們在這裏從新貼一下 HashMap 的 put 方法的源代碼:

HashMap.put:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}

重寫方法:

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
        }
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 調用create方法,將新元素以雙向鏈表的的形式加入到映射中。
    createEntry(hash, key, value, bucketIndex);

    // 刪除最近最少使用元素的策略定義
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    } else {
        if (size >= threshold)
            resize(2 * table.length);
    }
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
    table[bucketIndex] = e;
    // 調用元素的addBrefore方法,將元素加入到哈希、雙向連接列表。  
    e.addBefore(header);
    size++;
}

private void addBefore(Entry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}

讀取

LinkedHashMap 重寫了父類 HashMap 的 get 方法,實際在調用父類 getEntry() 方法取得查找的元素後,再判斷當排序模式 accessOrder 爲 true 時,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭,並從原來的位置刪除。因爲的鏈表的增長、刪除操做是常量級的,故並不會帶來性能的損失。

public V get(Object key) {
    // 調用父類HashMap的getEntry()方法,取得要查找的元素。
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    // 記錄訪問順序。
    e.recordAccess(this);
    return e.value;
}
public void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    // 若是定義了LinkedHashMap的迭代順序爲訪問順序,
    // 則刪除之前位置上的元素,並將最新訪問的元素添加到鏈表表頭。  
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
    }
}

 

/**
* Removes this entry from the linked list.
*/
private void remove() {
    before.after = after;
    after.before = before;
}

/**clear鏈表,設置header爲初始狀態*/
public void clear() {
 super.clear();
 header.before = header.after = header;
}

排序模式

LinkedHashMap 定義了排序模式 accessOrder,該屬性爲 boolean 型變量,對於訪問順序,爲 true;對於插入順序,則爲 false。通常狀況下,沒必要指定排序模式,其迭代順序即爲默認爲插入順序。

這些構造方法都會默認指定排序模式爲插入順序。若是你想構造一個 LinkedHashMap,並打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素,那麼請使用下面的構造方法構造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

該哈希映射的迭代順序就是最後訪問其條目的順序,這種映射很適合構建 LRU 緩存。LinkedHashMap 提供了 removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法能夠提供在每次添加新條目時移除最舊條目的實現程序,默認返回 false,這樣,此映射的行爲將相似於正常映射,即永遠不能移除最舊的元素。

咱們會在後面的文章中詳細介紹關於如何用 LinkedHashMap 構建 LRU 緩存。

總結

其實 LinkedHashMap 幾乎和 HashMap 同樣:從技術上來講,不一樣的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 裏,它是額外獨立出來的。LinkedHashMap 經過繼承 hashMap 中的 Entry<K,V>,並添加兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。

在寫關於 LinkedHashMap 的過程當中,記起來以前面試的過程當中遇到的一個問題,也是問我 Map 的哪一種實現能夠作到按照插入順序進行迭代?當時腦子是忽然短路的,但如今想一想,也只能怪本身對這個知識點仍是掌握的不夠紮實,因此又從頭認真的把代碼看了一遍。

不過,個人建議是,你們首先首先須要記住的是:LinkedHashMap 可以作到按照插入順序或者訪問順序進行迭代,這樣在咱們之後的開發中遇到類似的問題,才能想到用 LinkedHashMap 來解決,不然就算對其內部結構很是瞭解,不去使用也是沒有什麼用的。

LinkedHashSet 的實現原理

LinkedHashSet 概述

思考了很久,到底要不要總結 LinkedHashSet 的內容 = = 我在以前的博文中,分別寫了 HashMap 和 HashSet,而後咱們能夠看到 HashSet 的方法基本上都是基於 HashMap 來實現的,說白了,HashSet內部的數據結構就是一個 HashMap,其方法的內部幾乎就是在調用 HashMap 的方法。

LinkedHashSet 首先咱們須要知道的是它是一個 Set 的實現,因此它其中存的確定不是鍵值對,而是值。此實現與 HashSet 的不一樣之處在於,LinkedHashSet 維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。

看到上面的介紹,是否是感受其與 HashMap 和 LinkedHashMap 的關係很像?

注意,此實現不是同步的。若是多個線程同時訪問連接的哈希Set,而其中至少一個線程修改了該 Set,則它必須保持外部同步。

小 Demo

在LinkedHashMap的實現原理中,經過例子演示了 HashMap 和 LinkedHashMap 的區別。觸類旁通,咱們如今學習的LinkedHashSet與以前的很相同,只不過以前存的是鍵值對,而如今存的只有值。

因此我就再也不具體的貼代碼在這邊了,但咱們能夠確定的是,LinkedHashSet 是能夠按照插入順序或者訪問順序進行迭代。

LinkedHashSet 的實現

對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。

LinkedHashSet 底層使用 LinkedHashMap 來保存全部元素,它繼承與 HashSet,其全部的方法操做上又與 HashSet 相同,所以 LinkedHashSet 的實現上很是簡單,只提供了四個構造方法,並經過傳遞一個標識參數,調用父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操做上與父類 HashSet 的操做相同,直接調用父類 HashSet 的方法便可。LinkedHashSet 的源代碼以下:

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;

    /**
     * 構造一個帶有指定初始容量和加載因子的新空連接哈希set。
     *
     * 底層會調用父類的構造方法,構造一個有指定初始容量和加載因子的LinkedHashMap實例。
     * @param initialCapacity 初始容量。
     * @param loadFactor 加載因子。
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    /**
     * 構造一個帶指定初始容量和默認加載因子0.75的新空連接哈希set。
     *
     * 底層會調用父類的構造方法,構造一個帶指定初始容量和默認加載因子0.75的LinkedHashMap實例。
     * @param initialCapacity 初始容量。
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    /**
     * 構造一個帶默認初始容量16和加載因子0.75的新空連接哈希set。
     *
     * 底層會調用父類的構造方法,構造一個帶默認初始容量16和加載因子0.75的LinkedHashMap實例。
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }

    /**
     * 構造一個與指定collection中的元素相同的新連接哈希set。
     *
     * 底層會調用父類的構造方法,構造一個足以包含指定collection
     * 中全部元素的初始容量和加載因子爲0.75的LinkedHashMap實例。
     * @param c 其中的元素將存放在此set中的collection。
     */
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }
}

以上幾乎就是 LinkedHashSet 的所有代碼了,那麼讀者可能就會懷疑了,不是說 LinkedHashSet 是基於 LinkedHashMap 實現的嗎?那我爲何在源碼中甚至都沒有看到出現過 LinkedHashMap。不要着急,咱們能夠看到在 LinkedHashSet 的構造方法中,其調用了父類的構造方法。咱們能夠進去看一下:

/**
     * 以指定的initialCapacity和loadFactor構造一個新的空連接哈希集合。
     * 此構造函數爲包訪問權限,不對外公開,實際只是是對LinkedHashSet的支持。
     *
     * 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
     * @param initialCapacity 初始容量。
     * @param loadFactor 加載因子。
     * @param dummy 標記。
     */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

在父類 HashSet 中,專爲 LinkedHashSet 提供的構造方法以下,該方法爲包訪問權限,並未對外公開。

由上述源代碼可見,LinkedHashSet 經過繼承 HashSet,底層使用 LinkedHashMap,以很簡單明瞭的方式來實現了其自身的全部功能。

總結

以上就是關於 LinkedHashSet 的內容,咱們只是從概述上以及構造方法這幾個方面介紹了,並非咱們不想去深刻其讀取或者寫入方法,而是其自己沒有實現,只是繼承於父類 HashSet 的方法。

因此咱們須要注意的點是:

  • LinkedHashSet 是 Set 的一個具體實現,其維護着一個運行於全部條目的雙重連接列表。此連接列表定義了迭代順序,該迭代順序可爲插入順序或是訪問順序。
  • LinkedHashSet 繼承與 HashSet,而且其內部是經過 LinkedHashMap 來實現的。有點相似於咱們以前說的LinkedHashMap 其內部是基於 Hashmap 實現同樣,不過仍是有一點點區別的(具體的區別你們能夠本身去思考一下)。
  • 若是咱們須要迭代的順序爲插入順序或者訪問順序,那麼 LinkedHashSet 是須要你首先考慮的。

 

ArrayList 的實現原理

ArrayList 概述

ArrayList 能夠理解爲動態數組,用 MSDN 中的說法,就是 Array 的複雜版本。與 Java 中的數組相比,它的容量能動態增加。ArrayList 是 List 接口的可變數組的實現。實現了全部可選列表操做,並容許包括 null 在內的全部元素。除了實現 List 接口外,此類還提供一些方法來操做內部用來存儲列表的數組的大小。(此類大體上等同於 Vector 類,除了此類是不一樣步的。)

每一個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加。自動增加會帶來數據向新數組的從新拷貝,所以,若是可預知數據量的多少,可在構造 ArrayList 時指定其容量。在添加大量元素前,應用程序也可使用 ensureCapacity 操做來增長 ArrayList 實例的容量,這能夠減小遞增式再分配的數量。

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

咱們先學習瞭解其內部的實現原理,才能更好的理解其應用。

ArrayList 的實現

對於 ArrayList 而言,它實現 List 接口、底層使用數組保存全部元素。其操做基本上是對數組的操做。下面咱們來分析 ArrayList 的源代碼:

實現的接口

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}

ArrayList 繼承了 AbstractList,實現了 List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。

ArrayList 實現了 RandmoAccess 接口,即提供了隨機訪問功能。RandmoAccess 是 java 中用來被 List 實現,爲 List 提供快速訪問功能的。在 ArrayList 中,咱們便可以經過元素的序號快速獲取元素對象;這就是快速隨機訪問。

ArrayList 實現了 Cloneable 接口,即覆蓋了函數 clone(),能被克隆。 ArrayList 實現 java.io.Serializable 接口,這意味着 ArrayList 支持序列化,能經過序列化去傳輸。

底層使用數組實現

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;

構造方法

  /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this(10);
    }
    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }
ArrayList 提供了三種方式的構造器:

public ArrayList()能夠構造一個默認初始容量爲10的空列表;
public ArrayList(int initialCapacity)構造一個指定初始容量的空列表;
public ArrayList(Collection<? extends E> c)構造一個包含指定 collection 的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。

存儲

ArrayList 中提供了多種添加元素的方法,下面將一一進行講解:

1.set(int index, E element):該方法首先調用rangeCheck(index)來校驗 index 變量是否超出數組範圍,超出則拋出異常。然後,取出原 index 位置的值,而且將新的 element 放入 Index 位置,返回 oldValue。

   /**
     * Replaces the element at the specified position in this list with
     * the specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    /**
      * Checks if the given index is in range.  If not, throws an appropriate
      * runtime exception.  This method does *not* check if the index is
      * negative: It is always used immediately prior to an array access,
      * which throws an ArrayIndexOutOfBoundsException if index is negative.
      */
      private void rangeCheck(int index) {
        if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
      }

2.add(E e):該方法是將指定的元素添加到列表的尾部。當容量不足時,會調用 grow 方法增加容量。

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

3.add(int index, E element):在 index 位置插入 element。

 /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

4.addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c):將特定 Collection 中的元素添加到 Arraylist 末尾。

/**
     * Appends all of the elements in the specified collection to the end of
     * this list, in the order that they are returned by the
     * specified collection's Iterator.  The behavior of this operation is
     * undefined if the specified collection is modified while the operation
     * is in progress.  (This implies that the behavior of this call is
     * undefined if the specified collection is this list, and this
     * list is nonempty.)
     *
     * @param c collection containing elements to be added to this list
     * @return <tt>true</tt> if this list changed as a result of the call
     * @throws NullPointerException if the specified collection is null
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * Inserts all of the elements in the specified collection into this
     * list, starting at the specified position.  Shifts the element
     * currently at that position (if any) and any subsequent elements to
     * the right (increases their indices).  The new elements will appear
     * in the list in the order that they are returned by the
     * specified collection's iterator.
     *
     * @param index index at which to insert the first element from the
     *              specified collection
     * @param c collection containing elements to be added to this list
     * @return <tt>true</tt> if this list changed as a result of the call
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @throws NullPointerException if the specified collection is null
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

在 ArrayList 的存儲方法,其核心本質是在數組的某個位置將元素添加進入。但其中又會涉及到關於數組容量不夠而增加等因素。

讀取

這個方法就比較簡單了,ArrayList 可以支持隨機訪問的緣由也是很顯然的,由於它內部的數據結構是數組,而數組自己就是支持隨機訪問。該方法首先會判斷輸入的index值是否越界,而後將數組的 index 位置的元素返回便可。

/**
* Returns the element at the specified position in this list.
*
* @param  index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
    rangeCheck(index);
    return (E) elementData[index];
}
private void rangeCheck(int index) {
    if (index >= size)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

刪除

ArrayList 提供了根據下標或者指定對象兩種方式的刪除功能。須要注意的是該方法的返回值並不相同,以下:

  /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work

        return oldValue;
    }
/**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

注意:從數組中移除元素的操做,也會致使被移除的元素之後的全部元素的向左移動一個位置。

調整數組容量

從上面介紹的向 ArrayList 中存儲元素的代碼中,咱們看到,每當向數組中添加元素時,都要去檢查添加後元素的個數是否會超出當前數組的長度,若是超出,數組將會進行擴容,以知足添加數據的需求。數組擴容有兩個方法,其中開發者能夠經過一個 public 的方法ensureCapacity(int minCapacity)來增長 ArrayList 的容量,而在存儲元素等操做過程當中,若是遇到容量不足,會調用priavte方法private void ensureCapacityInternal(int minCapacity)實現。

    public void ensureCapacity(int minCapacity) {
        if (minCapacity > 0)
            ensureCapacityInternal(minCapacity);
    }

    private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

從上述代碼中能夠看出,數組進行擴容時,會將老數組中的元素從新拷貝一份到新的數組中,每次數組容量的增加大約是其原容量的 1.5 倍(從int newCapacity = oldCapacity + (oldCapacity >> 1)這行代碼得出)。這種操做的代價是很高的,所以在實際使用時,咱們應該儘可能避免數組容量的擴張。當咱們可預知要保存的元素的多少時,要在構造 ArrayList 實例時,就指定其容量,以免數組擴容的發生。或者根據實際需求,經過調用ensureCapacity 方法來手動增長 ArrayList 實例的容量。

Fail-Fast 機制

ArrayList 也採用了快速失敗的機制,經過記錄 modCount 參數來實現。在面對併發的修改時,迭代器很快就會徹底失敗,而不是冒着在未來某個不肯定時間發生任意不肯定行爲的風險。 關於 Fail-Fast 的更詳細的介紹,我在以前將 HashMap 中已經提到。

LinkedList 的實現原理

概述

LinkedList 和 ArrayList 同樣,都實現了 List 接口,但其內部的數據結構有本質的不一樣。LinkedList 是基於鏈表實現的(經過名字也能區分開來),因此它的插入和刪除操做比 ArrayList 更加高效。但也是因爲其爲基於鏈表的,因此隨機訪問的效率要比 ArrayList 差。

看一下 LinkedList 的類的定義:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}

LinkedList 繼承自 AbstractSequenceList,實現了 List、Deque、Cloneable、java.io.Serializable 接口。AbstractSequenceList 提供了List接口骨幹性的實現以減小實現 List 接口的複雜度,Deque 接口定義了雙端隊列的操做。

在 LinkedList 中除了自己本身的方法外,還提供了一些可使其做爲棧、隊列或者雙端隊列的方法。這些方法可能彼此之間只是名字不一樣,以使得這些名字在特定的環境中顯得更加合適。

LinkedList 也是 fail-fast 的(前邊提過不少次了)。

LinkedList 源碼解讀

數據結構

LinkedList 是基於鏈表結構實現,因此在類中包含了 first 和 last 兩個指針(Node)。Node 中包含了上一個節點和下一個節點的引用,這樣就構成了雙向的鏈表。每一個 Node 只能知道本身的前一個節點和後一個節點,但對於鏈表來講,這已經足夠了。

 transient int size = 0;
    transient Node<E> first; //鏈表的頭指針
    transient Node<E> last; //尾指針
    //存儲對象的結構 Node, LinkedList的內部類
    private static class Node<E> {
        E item;
        Node<E> next; // 指向下一個節點
        Node<E> prev; //指向上一個節點

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

存儲

add(E e)

該方法是在鏈表的 end 添加元素,其調用了本身的方法 linkLast(E e)。

該方法首先將 last 的 Node 引用指向了一個新的 Node(l),而後根據l新建了一個 newNode,其中的元素就爲要添加的 e;然後,咱們讓 last 指向了 newNode。接下來是自身進行維護該鏈表。

/**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

add(int index, E element)

該方法是在指定 index 位置插入元素。若是 index 位置正好等於 size,則調用 linkLast(element) 將其插入末尾;不然調用 linkBefore(element, node(index))方法進行插入。該方法的實如今下面,你們能夠本身仔細的分析一下。(分析鏈表的時候最好可以邊畫圖邊分析)

/**
    * Inserts the specified element at the specified position in this list.
    * Shifts the element currently at that position (if any) and any
    * subsequent elements to the right (adds one to their indices).
    *
    * @param index index at which the specified element is to be inserted
    * @param element element to be inserted
    * @throws IndexOutOfBoundsException {@inheritDoc}
    */
   public void add(int index, E element) {
       checkPositionIndex(index);

       if (index == size)
           linkLast(element);
       else
           linkBefore(element, node(index));
   }
   /**
        * Inserts element e before non-null Node succ.
        */
       void linkBefore(E e, Node<E> succ) {
           // assert succ != null;
           final Node<E> pred = succ.prev;
           final Node<E> newNode = new Node<>(pred, e, succ);
           succ.prev = newNode;
           if (pred == null)
               first = newNode;
           else
               pred.next = newNode;
           size++;
           modCount++;
       }

LinkedList 的方法實在是太多,在這無法一一舉例分析。但不少方法其實都只是在調用別的方法而已,因此建議你們將其幾個最核心的添加的方法搞懂就能夠了,好比 linkBefore、linkLast。其本質也就是鏈表之間的刪除添加等。

ConcurrentHashMap 的實現原理

概述

咱們在以前的博文中瞭解到關於 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當咱們只有一個線程在使用 HashMap 的時候,天然不會有問題,但若是涉及到多個線程,而且有讀有寫的過程當中,HashMap 就不能知足咱們的須要了(fail-fast)。在不考慮性能問題的時候,咱們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構作鎖定操做的,這樣在鎖表的期間,別的線程就須要等待了,無疑性能不高。

因此咱們在本文中學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。

ConcurrentHashMap 的實現是依賴於 Java 內存模型,因此咱們在瞭解 ConcurrentHashMap 的前提是必須瞭解Java 內存模型。但 Java 內存模型並非本文的重點,因此我假設讀者已經對 Java 內存模型有所瞭解。

ConcurrentHashMap 分析

ConcurrentHashMap 的結構是比較複雜的,都深究去本質,其實也就是數組和鏈表而已。咱們由淺入深慢慢的分析其結構。

先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap 的內部類,而後在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(相似於 HashMap 中 Entry),因此 HashEntry 能夠構成一個鏈表。

因此通俗的講,ConcurrentHashMap 數據結構爲一個 Segment 數組,Segment 的數據結構爲 HashEntry 的數組,而 HashEntry 存的是咱們的鍵值對,能夠構成鏈表。

首先,咱們看一下 HashEntry 類。

HashEntry

HashEntry 用來封裝散列映射表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被聲明爲 final 型,value 域被聲明爲 volatile 型。其類的定義爲:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

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

HashEntry 的學習能夠類比着 HashMap 中的 Entry。咱們的存儲鍵值對的過程當中,散列的時候若是發生「碰撞」,將採用「分離鏈表法」來處理碰撞:把碰撞的 HashEntry 對象連接成一個鏈表。

以下圖,咱們在一個空桶中插入 A、B、C 兩個 HashEntry 對象後的結構圖(其實應該爲鍵值對,在這進行了簡化以方便更容易理解):

圖1

Segment

Segment 的類定義爲static final class Segment<K,V> extends ReentrantLock implements Serializable。其繼承於 ReentrantLock 類,從而使得 Segment 對象能夠充當鎖的角色。Segment 中包含HashEntry 的數組,其能夠守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點相似於 HashMap了,都是包含了一個數組,而數組中的元素能夠是一個鏈表。

table:table 是由 HashEntry 對象組成的數組若是散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式連接成一個鏈表table數組的數組成員表明散列映射表的一個桶每一個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分若是併發級別爲 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。

count 變量是計算器,表示每一個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之因此在每一個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是爲了不出現「熱點域」而影響併發性。

/**
     * Segments are specialized versions of hash tables.  This
     * subclasses from ReentrantLock opportunistically, just to
     * simplify some locking and avoid separate construction.
     */
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
      /**
         * The per-segment table. Elements are accessed via
         * entryAt/setEntryAt providing volatile semantics.
         */
        transient volatile HashEntry<K,V>[] table;

        /**
         * The number of elements. Accessed only either within locks
         * or among other volatile reads that maintain visibility.
         */
        transient int count;
        transient int modCount;
        /**
         * 裝載因子
         */
        final float loadFactor;
    }

咱們經過下圖來展現一下插入 ABC 三個節點後,Segment 的示意圖:

圖2

其實從我我的角度來講,Segment結構是與HashMap很像的。

ConcurrentHashMap

ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的併發級別會建立包含 16 個 Segment 對象的數組。經過咱們上面的知識,咱們知道每一個 Segment 又包含若干個散列表的桶,每一個桶是由 HashEntry 連接起來的一個鏈表。若是 key 可以均勻散列,每一個 Segment 大約守護整個散列表桶總數的 1/16。

下面咱們還有經過一個圖來演示一下 ConcurrentHashMap 的結構:

圖3

併發寫操做

在 ConcurrentHashMap 中,當執行 put 方法的時候,會須要加鎖來完成。咱們經過代碼來解釋一下具體過程: 當咱們 new 一個 ConcurrentHashMap 對象,而且執行put操做的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法源碼爲:

/**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p> The value can be retrieved by calling the <tt>get</tt> method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>
     * @throws NullPointerException if the specified key or value is null
     */
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

咱們經過註釋能夠了解到,ConcurrentHashMap 不容許空值。該方法首先有一個 Segment 的引用 s,而後會經過 hash() 方法對 key 進行計算,獲得哈希值;繼而經過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操做。該方法源碼爲:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //加鎖,這裏是鎖定的Segment而不是整個ConcurrentHashMap
    HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        //獲得hash對應的table中的索引index
        int index = (tab.length - 1) & hash;
        //找到hash對應的是具體的哪一個桶,也就是哪一個HashEntry鏈表
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        //解鎖
        unlock();
    }
    return oldValue;
}

關於該方法的某些關鍵步驟,在源碼上加上了註釋。

須要注意的是:加鎖操做是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。由於 put 操做只是在這個 Segment 中完成,因此並不須要對整個 ConcurrentHashMap 加鎖。因此,此時,其餘的線程也能夠對另外的 Segment 進行 put 操做,由於雖然該 Segment 被鎖住了,但其餘的 Segment 並無加鎖。同時,讀線程並不會由於本線程的加鎖而阻塞。

正是由於其內部的結構以及機制,因此 ConcurrentHashMap 在併發訪問的性能上要比Hashtable和同步包裝以後的HashMap的性能提升不少。在理想狀態下,ConcurrentHashMap 能夠支持 16 個線程執行併發寫操做(若是併發級別設置爲 16),及任意數量線程的讀操做。

總結

在實際的應用中,散列表通常的應用場景是:除了少數插入操做和刪除操做外,絕大多數都是讀取操做,並且讀操做在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操做作了大量的優化。經過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操做不須要加鎖就能夠正確得到值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提升。

ConcurrentHashMap 是一個併發散列映射表的實現,它容許徹底併發的讀取,而且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不一樣線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也致使對容器的訪問變成串行化的了。

ConcurrentHashMap 的高併發性主要來自於三個方面:

  • 用分離鎖實現多個線程間的更深層次的共享訪問。
  • 用 HashEntery 對象的不變性來下降執行讀操做的線程在遍歷鏈表期間對加鎖的需求。
  • 經過對同一個 Volatile 變量的寫 / 讀訪問,協調不一樣線程間讀 / 寫操做的內存可見性。

使用分離鎖,減少了請求 同一個鎖的頻率。

經過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操做大多數時候不須要加鎖就能成功獲取到須要的值。因爲散列映射表在實際應用中大多數操做都是成功的 讀操做,因此 2 和 3 既能夠減小請求同一個鎖的頻率,也能夠有效減小持有鎖的時間。經過減少請求同一個鎖的頻率和儘可能減小持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提升。

LinkedHashMap 與 LRUcache

LRU 緩存介紹

咱們平時總會有一個電話本記錄全部朋友的電話,可是,若是有朋友常常聯繫,那些朋友的電話號碼不用翻電話本咱們也能記住,可是,若是長時間沒有聯繫了,要再次聯繫那位朋友的時候,咱們又不得不求助電話本,可是,經過電話本查找仍是很費時間的。可是,咱們大腦可以記住的東西是必定的,咱們只能記住本身最熟悉的,而長時間不熟悉的天然就忘記了。

其實,計算機也用到了一樣的一個概念,咱們用緩存來存放之前讀取的數據,而不是直接丟掉,這樣,再次讀取的時候,能夠直接在緩存裏面取,而不用再從新查找一遍,這樣系統的反應能力會有很大提升。可是,當咱們讀取的個數特別大的時候,咱們不可能把全部已經讀取的數據都放在緩存裏,畢竟內存大小是必定的,咱們通常把最近常讀取的放在緩存裏(至關於咱們把最近聯繫的朋友的姓名和電話放在大腦裏同樣)。

LRU 緩存利用了這樣的一種思想。LRU 是 Least Recently Used 的縮寫,翻譯過來就是「最近最少使用」,也就是說,LRU 緩存把最近最少使用的數據移除,讓給最新讀取的數據。而每每最常讀取的,也是讀取次數最多的,因此,利用 LRU 緩存,咱們可以提升系統的 performance。

實現

要實現 LRU 緩存,咱們首先要用到一個類 LinkedHashMap。

用這個類有兩大好處:一是它自己已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最最不常讀取的會放在最後(固然,它也能夠實現按照插入順序存儲)。第二,LinkedHashMap 自己有一個方法用於判斷是否須要移除最不常讀取的數,可是,原始方法默認不須要移除(這是,LinkedHashMap 至關於一個linkedlist),因此,咱們須要 override 這樣一個方法,使得當緩存裏存放的數據個數超過規定個數後,就把最不經常使用的移除掉。關於 LinkedHashMap 中已經有詳細的介紹。

代碼以下:(可直接複製,也能夠經過LRUcache-Java下載)

import java.util.LinkedHashMap;
import java.util.Collection;
import java.util.Map;
import java.util.ArrayList;

/**
 * An LRU cache, based on <code>LinkedHashMap</code>.
 *
 * <p>
 * This cache has a fixed maximum number of elements (<code>cacheSize</code>).
 * If the cache is full and another entry is added, the LRU (least recently
 * used) entry is dropped.
 *
 * <p>
 * This class is thread-safe. All methods of this class are synchronized.
 *
 * <p>
 * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
 * Multi-licensed: EPL / LGPL / GPL / AL / BSD.
 */
public class LRUCache<K, V> {
    private static final float hashTableLoadFactor = 0.75f;
    private LinkedHashMap<K, V> map;
    private int cacheSize;

    /**
     * Creates a new LRU cache. 在該方法中,new LinkedHashMap<K,V>(hashTableCapacity,
     * hashTableLoadFactor, true)中,true表明使用訪問順序
     *
     * @param cacheSize
     *            the maximum number of entries that will be kept in this cache.
     */
    public LRUCache(int cacheSize) {
        this.cacheSize = cacheSize;
        int hashTableCapacity = (int) Math
                .ceil(cacheSize / hashTableLoadFactor) + 1;
        map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor,
                true) {
            // (an anonymous inner class)
            private static final long serialVersionUID = 1;

            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > LRUCache.this.cacheSize;
            }
        };
    }

    /**
     * Retrieves an entry from the cache.<br>
     * The retrieved entry becomes the MRU (most recently used) entry.
     *
     * @param key
     *            the key whose associated value is to be returned.
     * @return the value associated to this key, or null if no value with this
     *         key exists in the cache.
     */
    public synchronized V get(K key) {
        return map.get(key);
    }

    /**
     * Adds an entry to this cache. The new entry becomes the MRU (most recently
     * used) entry. If an entry with the specified key already exists in the
     * cache, it is replaced by the new entry. If the cache is full, the LRU
     * (least recently used) entry is removed from the cache.
     *
     * @param key
     *            the key with which the specified value is to be associated.
     * @param value
     *            a value to be associated with the specified key.
     */
    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    /**
     * Clears the cache.
     */
    public synchronized void clear() {
        map.clear();
    }

    /**
     * Returns the number of used entries in the cache.
     *
     * @return the number of entries currently in the cache.
     */
    public synchronized int usedEntries() {
        return map.size();
    }

    /**
     * Returns a <code>Collection</code> that contains a copy of all cache
     * entries.
     *
     * @return a <code>Collection</code> with a copy of the cache content.
     */
    public synchronized Collection<Map.Entry<K, V>> getAll() {
        return new ArrayList<Map.Entry<K, V>>(map.entrySet());
    }

    // Test routine for the LRUCache class.
    public static void main(String[] args) {
        LRUCache<String, String> c = new LRUCache<String, String>(3);
        c.put("1", "one"); // 1
        c.put("2", "two"); // 2 1
        c.put("3", "three"); // 3 2 1
        c.put("4", "four"); // 4 3 2
        if (c.get("2") == null)
            throw new Error(); // 2 4 3
        c.put("5", "five"); // 5 2 4
        c.put("4", "second four"); // 4 5 2
        // Verify cache content.
        if (c.usedEntries() != 3)
            throw new Error();
        if (!c.get("4").equals("second four"))
            throw new Error();
        if (!c.get("5").equals("five"))
            throw new Error();
        if (!c.get("2").equals("two"))
            throw new Error();
        // List cache content.
        for (Map.Entry<String, String> e : c.getAll())
            System.out.println(e.getKey() + " : " + e.getValue());
    }
}


HashSet 和 HashMap 的比較

HashMap 和 HashSet 都是 collection 框架的一部分,它們讓咱們可以使用對象的集合。collection 框架有本身的接口和實現,主要分爲 Set 接口,List 接口和 Queue 接口。它們有各自的特色,Set 的集合裏不容許對象有重複的值,List 容許有重複,它對集合中的對象進行索引,Queue 的工做原理是 FCFS 算法(First Come, First Serve)。

首先讓咱們來看看什麼是 HashMap 和 HashSet,而後再來比較它們之間的分別。

什麼是 HashSet

HashSet 實現了 Set 接口,它不容許集合中有重複的值,當咱們提到 HashSet 時,第一件事情就是在將對象存儲在 HashSet 以前,要先確保對象重寫 equals()和 hashCode()方法,這樣才能比較對象的值是否相等,以確保set中沒有儲存相等的對象。若是咱們沒有重寫這兩個方法,將會使用這個方法的默認實現。

public boolean add(Object o)方法用來在 Set 中添加元素,當元素值重複時則會當即返回 false,若是成功添加的話會返回 true。

什麼是 HashMap

HashMap 實現了 Map 接口,Map 接口對鍵值對進行映射。Map 中不容許重複的鍵。Map 接口有兩個基本的實現,HashMap 和 TreeMap。TreeMap 保存了對象的排列次序,而 HashMap 則不能。HashMap 容許鍵和值爲 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保證 HashMap synchronized,這樣多個線程同時訪問 HashMap 時,能保證只有一個線程更改 Map。

public Object put(Object Key,Object value)方法用來將元素添加到 map 中。

HashSet 和 HashMap 的區別

HashMap HashSet
HashMap實現了Map接口 HashSet實現了Set接口
HashMap儲存鍵值對 HashSet僅僅存儲對象
使用put()方法將元素放入map中 使用add()方法將元素放入set中
HashMap中使用鍵對象來計算hashcode值 HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false
HashMap比較快,由於是使用惟一的鍵來獲取對象 HashSet較HashMap來講比較慢
相關文章
相關標籤/搜索