HashMap原始碼解析(基於JDK1.7)

LiuJian-Android發表於2018-01-02

一 簡介

java.lang.Object  

↳     java.util.AbstractMap  

↳           java.util.HashMap  

public class HashMap  

extends AbstractMap  

implements Map, Cloneable, Serializable { }  
複製程式碼

HashMap是基於雜湊表實現的,每一個元素是一個key-value對,實現了Serializable、Cloneable介面,允許使用null值和null鍵。不保證對映的順序,內部通過單連結串列解決衝突問題,容量超過(容量*載入因子)時,會自動增長。(除了不同步和允許使用null之外,HashMap類與Hashtable大致相同)
HashMap不是執行緒安全的,如果想獲取執行緒安全的HashMap

  • 1通過Collections類的靜態方法synchronizedMap獲得執行緒安全的HashMap。
      Map map = Collections.synchronizedMap(new HashMap());
    複製程式碼
  • 2使用concurrent併發包下的concurrentHashMap。

二 資料結構

HashMap由陣列+連結串列組成的,主幹是一個Entry陣列,每一個entry包含一個(key-value)鍵值對,連結串列則是主要為了解決雜湊衝突而存在的,HashMap通過key的hashCode來計算hash值,當hashCode相同時,通過“拉鍊法”解決衝突,如下圖所示。

HashMap原始碼解析(基於JDK1.7)
如果定位到的陣列位置不含連結串列(當前entry的next指向null),那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度依然為O(1),因為最新的Entry會插入連結串列頭部,只需要簡單改變引用鏈即可,而對於查詢操作來講,此時就需要遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以效能考慮,HashMap中的連結串列出現越少,效能才會越好。

三 原始碼分析

1 關鍵屬性

//預設初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量,即2的30次方  
static final int MAXIMUM_CAPACITY = 1 << 30;  
//預設載入因子,當容器使用率達到75%的時候就擴容
static final float DEFAULT_LOAD_FACTOR = 0.75f;  
//HashMap內部的儲存結構是一個陣列,此處陣列為空,即沒有初始化之前的狀態  
static final Entry<?,?>[] EMPTY_TABLE = {};  
//空的儲存實體  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  
//實際儲存的key-value鍵值對的個數
transient int size;
//擴容的臨界點,如果當前容量達到該值,則需要擴容了.
//如果當前陣列容量為0時(空陣列),則該值作為初始化內部陣列的初始容量
int threshold;
//由建構函式傳入的指定負載因子
final float loadFactor;
//修改次數,用於快速失敗機制
transient int modCount;
//預設的threshold值  
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
複製程式碼

2 構造方法

構造方法主要完成容量和載入因子的設定

      /**
     * 通過初始容量和狀態因子構造HashMap 
     * @param initialCapacity 容量
     * @param loadFactor 載入因子
     */
    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);  

        this.loadFactor = loadFactor;  
        threshold = initialCapacity;  
        //init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
        init();
    }  
複製程式碼
   /**
     * 通過擴容因子構造HashMap,容量去預設值,即16 
     */
    public HashMap(int initialCapacity) {  
        this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    }  
複製程式碼
      /**
     * 載入因子取0.75,容量取16,構造HashMap 
     */
    public HashMap() {  
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
    }  
複製程式碼
    /**
     * 通過其他Map來初始化HashMap,容量通過其他Map的size來計算,載入因子取0.75
     */
    public HashMap(Map<? extends K, ? extends V> m) {  
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
        //初始化HashMap底層的陣列結構
        inflateTable(threshold);
        //新增m中的元素
        putAllForCreate(m);  
    }  
複製程式碼

3 儲存資料(put)

    /**
     * 存入一個鍵值對,如果key重複,則更新value
     * @param key 鍵值名
     * @param value 鍵值
     * @return 如果存的是新key則返回null,如果覆蓋了舊鍵值對,則返回舊value
     */
    public V put(K key, V value) {
        //如果陣列為空,則新建陣列
        if (table == EMPTY_TABLE) {
         //初始化HashMap底層的陣列結構
            inflateTable(threshold);
        }
        //如果key為null,則把value放在table[0]中
        if (key == null)
            return putForNullKey(value);
        //生成key所對應的hash值
        int hash = hash(key);
        //根據hash值和陣列的長度找到:該key所屬entry在table中的位置i
        int i = indexFor(hash, table.length);
        /**
         * 陣列中每一項存的都是一個連結串列,
         * 先找到i位置,然後迴圈該位置上的每一個entry,
         * 如果發現存在key與傳入key相等,則替換其value。然後結束方法。
         * 如果沒有找到相同的key,則繼續執行下一條指令,將此鍵值對存入連結串列頭
         */
        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;
            }
        }
        //map操作次數加一
        modCount++;
        //檢視是否需要擴容,並將該鍵值對存入指定下標的連結串列頭中
        addEntry(hash, key, value, i);
        //如果是新存入的鍵值對,則返回null
        return null;
    }
複製程式碼
/**
  *  將該鍵值對存入指定下標的連結串列頭中
     * @param hash hash值
     * @param key 鍵值名
     * @param value 鍵值
     * @param bucketIndex 索引
  */
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //當size超過臨界閾值threshold,並且即將發生雜湊衝突時進行擴容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }
複製程式碼
 /**
     * 將鍵值對與他的hash值作為一個entry,插入table的指定下標中的連結串列頭中
     * @param hash hash值
     * @param key 鍵值名
     * @param value 鍵值
     * @param bucketIndex 索引
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
複製程式碼

4 hash演算法

static int hash(int h) {
    //此功能確保hash碼不同,有限數量的碰撞
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
複製程式碼

5 調整容量(resize)

 /**
     * 對陣列擴容,即建立一個新陣列,並將舊陣列裡的東西重新存入新陣列
     * @param newCapacity 新陣列容量
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length
        //如果當前陣列容量已經達到最大值了,則將擴容的臨界值設定為Integer.MAX_VALUE(Integer.MAX_VALUE是容量的臨界點)
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //建立一個擴容後的新陣列
        Entry[] newTable = new Entry[newCapacity];
        //將當前陣列中的鍵值對存入新陣列
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //用新陣列替換舊陣列
        table = newTable;
        //計算下一個擴容臨界點
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
複製程式碼
  /**
     * 將現有陣列中的內容重新通過hash計算存入新陣列
     * @param newTable 新陣列
     * @param rehash  
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍歷現有陣列中的每一個單連結串列的頭entry
        for (Entry<K,V> e : table) {
            //查詢連結串列裡的每一個entry
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //根據新的陣列長度,重新計算此entry所在下標i
                int i = indexFor(e.hash, newCapacity);
                //將entry放入下標i處連結串列的頭部(將新陣列此處的原有連結串列存入entry的next指標)
                e.next = newTable[i];
                //將連結串列存回下標i
                newTable[i] = e;
                //檢視下一個entry
                e = next;
            }
        }
    }
複製程式碼

6 資料讀取(get)

/**
     * 返回此hashmap中儲存的鍵值對個數
     * @return 鍵值對個數
     */
    public int size() {
        return size;
    }
複製程式碼
    /**
     * 根據key找到對應value
     * @param key 鍵值名
     * @return 鍵值value
     */
    public V get(Object key) {
        //如果key為null,則從table[0]中取value
        if (key == null)
            return getForNullKey();
        //如果key不為null,則先根據key,找到其entry
        Entry<K,V> entry = getEntry(key);
        //返回entry節點裡的value值
        return null == entry ? null : entry.getValue();
    }
複製程式碼
    /**
     * 返回一個set集合,裡面裝的都是hashmap的value。
     * 因為map中的key不能重複,set集合中的值也不能重複,所以可以裝入set。
     * 在hashmap的父類AbstractMap中,定義了Set<K> keySet = null;
     * 如果keySet為null,則返回內部類KeySet。
     * @return 含有所有key的set集合
     */
    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
複製程式碼
    /**
     * 返回一個Collection集合,裡面裝的都是hashmap的value。
     * 因為map中的value可以重複,所以裝入Collection。
     * 在hashmap的父類AbstractMap中,定義了Collection<V> values = null;
     * 如果values為null,則返回內部類Values。
     */
    public Collection<V> values() {
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()));
    }
複製程式碼

7 移除資料(clear remove)

    /**
     * 刪除hashmap中的所有元素
     */
    public void clear() {
        modCount++;
        //將table中的每一個元素都設定成null
        Arrays.fill(table, null);
        size = 0;
    }
複製程式碼
/**
     * 根據key刪除entry節點
     * @param key 被刪除的entry的key值
     * @return 被刪除的節點的value,刪除失敗則返回null
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
複製程式碼
/**
     * 根據key刪除entry節點
     * @param key 被刪除的entry的key值
     * @return 被刪除的節點,刪除失敗則返回null
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //計算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //計算所屬下標
        int i = indexFor(hash, table.length);
        //找到下標所儲存的單連結串列的頭節點
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        //迭代單連結串列找到要刪除的節點
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }
複製程式碼

8 Fail-Fast機制

“快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個執行緒對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。 注意 :記住是有可能,而不是一定。 例如:假設存在兩個執行緒(執行緒1、執行緒2),執行緒1通過Iterator在遍歷集合A中的元素,在某個時候執行緒2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),這個時候程式就會丟擲ConcurrentModificationException 異常,從而產生fail-fast機制。

四 JDK1.7和1.8 HashMap的區別

1 資料結構

HashMap原始碼解析(基於JDK1.7)
使用一個Node陣列來儲存資料,但這個Node可能是連結串列結構,也可能是紅黑樹結構。如果插入的key的hashcode相同,那麼這些key也會被定位到Node陣列的同一個桶位。如果同一個桶位裡的key不超過8個,使用連結串列結構儲存。如果超過了8個,那麼會呼叫treeifyBin函式,將連結串列轉換為紅黑樹。那麼即使hashcode完全相同,由於紅黑樹的特點,查詢某個特定元素,也只需要O(logn)的開銷。也就是說put/get的操作的時間複雜度最差只有O(log n)。
注意:有一個限制:key的物件,必須正確的實現了Compare介面。如果沒有實現Compare介面,或者實現得不正確(比方說所有Compare方法都返回0)。那JDK1.8的HashMap其實還是慢於JDK1.7的。

2 hash演算法

/**
   * @param key 
   * @return 
   */
static final int hash(Object key) {
     int h;
     // h = key.hashCode() 為第一步 取hashCode值
     // h ^ (h >>> 16)  為第二步 高位參與運算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼

相關文章