HashMap數據結構

2.1 HashMap

2.1.1 HashMap介紹

先看看HashMap類頭部的源碼:html

public class HashMap<K,V>java

    extends AbstractMap<K,V>算法

implements Map<K,V>, Cloneable, Serializable數組

HashMap基於哈希表的 Map 接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了非同步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。數據結構

此實現假定哈希函數將元素適當地分佈在各個桶(數組元素)之間,可爲基本操做(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的容量(桶的數量)及其大小(鍵-值映射關係數)成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高(或將負載因子設置得過低)。併發

HashMap 的實例有兩個參數影響其性能:初始容量和負載因子。容量是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。負載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。dom

一般,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數HashMap 類的操做中,包括get put 操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生rehash 操做。函數

若是不少映射關係要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操做以增大表的容量來講,使用足夠大的初始容量建立它將使得映射關係能更有效地存儲。源碼分析

注意,此實現不是同步的。若是多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的任何操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這通常經過對天然封裝該映射的對象進行同步操做來完成。若是不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來包裝該映射。最好在建立時完成這一操做,以防止對映射進行意外的非同步訪問,以下所示:性能

   Map m = Collections.synchronizedMap(new HashMap(...));

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

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

 

2.1.2 HashMap存儲結構圖

這裏先給出HashMap的存儲結構,在後面的源碼分析中,咱們將更加詳細的對此做介紹。HashMap採起數組加鏈表的存儲方式來實現。亦即數組(散列桶)中的每個元素都是鏈表,以下圖:

 

 

      圖2-1

說明:下面針對HashMap的源碼分析中,全部提到的桶或散列桶都表示存儲結構中數組的元素,桶或散列桶的數量亦即表示數組的長度,哈希碼亦即散列碼。

2.1.3 屬性分析

先來看看HashMap有哪些屬性,HashMap沒有從AbstractMap父親中繼承任何屬性,下面這些都是HashMap的屬性:

static final int DEFAULT_INITIAL_CAPACITY = 16;

DEFAULT_INITIAL_CAPACITY是HashMap默認的初始化桶數量,如圖2-1中所示。對於HashMap中桶數量的值必須是2N次冪,並且這個是HashMap強制規定的。這樣作的緣由就是由於計算機進行2次冪的運算是很是高效的,僅經過位移操做就能夠完成2N次冪的運算。

 

static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY是HashMap中散列桶數量的最大值,從上面的代碼可知這個最大值爲232次冪,即1073741824

static final float DEFAULT_LOAD_FACTOR = 0.75f;

默認的負載因子,若是在在建立HashMap的構造函數中沒有指定負載因子,則指定該HashMap的默認負載因子爲0.75,這意味着當HashMap中條目的數量達到了條目數量75%時,HashMap將進行resize操做以增長桶的數量。對於桶的擴展,等分析到下面的具體時會做更詳細的介紹。

transient Entry<K,V>[] table;

table就是HashMap的存儲結構,顯然這是一個數組,數組的每個元素都是一個條目(Entry),EntryHashMap中的一個內部類,它有以下4個屬性:final K key;V value;Entry<K,V> next;int hash。分別爲鍵、值、指向下一個鏈表結點的指針、散列(哈希)值。這就是圖2.1HashMap存儲結構的代碼實現。

transient int size;

size表示HashMap中條目(即鍵-值對)的數量。

int threshold;

threshold是HashMap的重構閾值,它的值爲容量和負載因子的乘積。在HashMap中全部桶中條目的總數量達到了這個重構閾值以後,HashMap將進行resize操做以自動擴容。

final float loadFactor;

loadFactor表示HashMap的負載因子,它和容量同樣都是HashMap擴容的決定性因素。

transient int modCount;

modCount表示HashMap被結構化更新的次數,好比插入、刪除、清空等會更新HashMap結構的操做次數。

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT  

= Integer.MAX_VALUE;

ALTERNATIVE_HASHING_THRESHOLD_DEFAULT表示在對字符串鍵(即keyString類型)的HashMap應用備選哈希函數時HashMap的條目數量的默認閾值。備選哈希函數的使用能夠減小因爲對字符串鍵進行弱哈希碼計算時的碰撞機率。

transient boolean useAltHashing;

useAltHashing表示是否要對字符串鍵的HashMap使用備選哈希函數。

transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

hashSeed表示一個與當前實例關聯而且能夠減小哈希碰撞機率應用於鍵的哈希碼計算的隨機種子。

2.1.4 構造分析

HashMap提供了4個構造方法,按照它們在源碼中的位置順序從上至下列出:

HashMap(int initialCapacity, float loadFactor)

HashMap(int initialCapacity)

HashMap()

HashMap(Map<? extends K, ? extends V> m)

 

(1) 咱們先來分析第一個同時傳遞初始化容量參數和負載因子參數的源碼,由於其它的3個構造方法都會調用這個構造方法,下面給出這個方法的代碼及分析:

public HashMap(int initialCapacity, float loadFactor) {

//部分構造參數容錯處理的源碼已省略...

    /**

     * 根據傳入的初始化容量計算該HashMap的容量(即桶的數量)

     * 算法爲:將capacity進行不斷的左移,直至capacity大於或等於初始化容量

    */

    int capacity = 1;

    while (capacity < initialCapacity)

        capacity <<= 1;

    //負載因子初始化

    this.loadFactor = loadFactor;

    /**

     * 條目閾值的計算

     * 算法:超出條目最大容量前取容量與負載因子的乘積做爲條目閾值

     */

    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

    //建立數組(散列桶)

    table = new Entry[capacity];

    //計算是否對字符串鍵的HashMap使用備選哈希函數

    useAltHashing = sun.misc.VM.isBooted() &&

        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

init();//調用初始化方法,默認狀況下什麼也沒作

}

(2) 下面是隻傳初始化容量參數的構造方法:

public HashMap(int initialCapacity) {

    //初始化容量傳入,加載因子爲默認值0.75f

    this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

(3) 下面是無參構造方法:

public HashMap() {

    //初始化容量爲默認值16,加載因子也爲默認值0.75f

    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

}

(4) 下面是根據已有Map構造新HashMap的構造方法:

public HashMap(Map<? extends K, ? extends V> m) {

    /**

     * 取下面兩個值的較大的值做爲當前要構造的HashMap的初始容量

     * 第1個值:用傳入的Map的條目數量除以默認加載因子再加上1

     * 第2個值:默認的初始化容量

     */

     this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,

          DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

     /**

      * 把傳入的map裏的全部條目放入當前已構造的HashMap中

      * 關於putAllForCreate方法後面會做分析

     */

     putAllForCreate(m);

}

2.1.5 hash方法

hash方法的源碼及分析以下:

final int hash(Object k) {

int h = 0;

/**

   * 若是useAltHashing的值爲true

     *      而且鍵的類型爲String,則對字符串鍵使用備選哈希函數

     *     不然,返回用於對鍵進行哈希碼計算的隨機種子hashSeed

     * 關於hashSeed在2.1.3.1小節中已介紹過,這裏再也不贅述

     */

if (useAltHashing) {

if (k instanceof String) {

return sun.misc.Hashing.stringHash32((String) k);

        }

        h = hashSeed;

    }

    /**

     * 對h和鍵的哈希碼進行抑或並賦值運算

     * 等價於h = h ^ k.hashCode();

     */

h ^= k.hashCode();

 

    //下面兩步的運算過程如圖2-2所示

    h ^= (h >>> 20) ^ (h >>> 12);

    return h ^ (h >>> 7) ^ (h >>> 4);

}

假設h=0x7FFFFFFF,則上面最後兩步對h的運算過程以下圖:

 

 

2-2 

 

2.1.6 indexFor方法

/**

 * h表示經過hash(Object k)方法計算得來的哈希碼

 * length表示桶的數量(即數組的長度)

 */

static int indexFor(int h, int length) {

/**

 * 將哈希碼和length進行按位與運算

 * 全部的h值都會在映射在閉區間[0,length-1]內

 * 不一樣的h值可能映射到閉區間[0,length-1]內同一個值上

 */

    return h & (length-1);

}

 

2.1.7 put方法

/**

* 在HashMap中存儲一個鍵值對,若指定的鍵已經存在於HashMap中

* 則將新的值替換掉舊值,不然新添加一個條目來存儲這個鍵值對

* @param key 指定的鍵

* @param value 指定的值

* @return 若該鍵已經存在則返回該鍵對應的舊值,不然返回null

*/

public V put(K key, V value) {

    if (key == null)

     /**

      * 若鍵爲null,則調用putForNullKey方法進行插入

      * putForNullKey的源碼這裏再也不分析,讀者有興趣能夠自行分析它的源碼

      */

        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方法添加向桶中添加新結點

     * addEntry方法下一小節將會詳細介紹

     */

    addEntry(hash, key, value, i);

    return null;

}

2.1.8 addEntry方法

/**

 * 向HashMap的指定桶中添加一個新的鍵對值

 * 若要對HashMap擴容(即增長桶的數量),則下面的方法可能會修改傳入的桶索引

 * @param hash 指定鍵對應的哈希碼

 * @param key 指定鍵

 * @param value 指定值

 * @param bucketIndex 桶索引

 */

void addEntry(int hash, K key, V value, int bucketIndex) {

    if ((size >= threshold) && (null != table[bucketIndex])) {

     //若是HashMap中條目的數量達到了重構閾值且指定的桶不爲null,則對HashMap進行擴容(即增長桶的數量)

     /**

      * 調用resize方法對HashMap進行擴容

      * 對於resize方法,下面會有專門的一小節來做介紹,這裏先不介紹

      */

        resize(2 * table.length);

        //擴容後,桶的數量增長了,故須要從新對鍵進行哈希碼的計算

        hash = (null != key) ? hash(key) : 0;

        

        //根據新的鍵哈希碼和新的桶數量從新計算桶索引值

        bucketIndex = indexFor(hash, table.length);

    }

    /**

     * 在指定的桶中建立一個新的條目以存儲咱們傳入的鍵值對

     * 對於createEntry方法,讀者如有興趣能夠自行閱讀其源碼

     */

    createEntry(hash, key, value, bucketIndex);

}

2.1.9 resize方法

/**

 * 從新調整HashMap中桶的數量

 * @param newCapacity 新的桶數量

 */

void resize(int newCapacity) {

/**

 * 下面的這段代碼對新值進行判斷

 * 若是新值超過了條目(Entry)數量的最大值

 * 則新int最大值賦值給重構閾值而後,而後直接返回而不會進行擴容

 */

    Entry[] oldTable = table;

    int oldCapacity = oldTable.length;

    if (oldCapacity == MAXIMUM_CAPACITY) {

        threshold = Integer.MAX_VALUE;

        return;

    }

    //若newCapacity合法,則新建一個桶數組。

    Entry[] newTable = new Entry[newCapacity];

    //計算是否須要對鍵從新進行哈希碼的計算

    boolean oldAltHashing = useAltHashing;

    useAltHashing |= sun.misc.VM.isBooted() &&

            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

    boolean rehash = oldAltHashing ^ useAltHashing;

    /**

     * 將原有全部的桶遷移至新的桶數組中

     * 在遷移時,桶在桶數組中的絕對位置可能會發生變化

     * 這就是爲何HashMap不能保證存儲條目的順序不能恆久不變的緣由

     * 讀者如有興趣,能夠自行閱讀transfer方法的源碼

     */

    transfer(newTable, rehash);

    //將新的桶數組的引用賦值給舊數組

    table = newTable;

    //像構造方法中同樣來從新計算重構閾值

    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}

2.1.10 get方法

/**

 * 根據指定鍵獲取該鍵對應的值

 * @param key 指定鍵

 * @return 若該鍵存在於HashMap中,則返回該鍵對應的值,不然返回null

 */

public V get(Object key) {

    if (key == null)

     //若鍵爲null,則返回null鍵對應的值

        return getForNullKey();

    //根據鍵獲取條目,下一小節會單獨介紹getEntry方法

    Entry<K,V> entry = getEntry(key);

    //返回條目的值,若條目爲null,則返回null

    return null == entry ? null : entry.getValue();

}

相關文章
相關標籤/搜索