1. 程式人生 > >HashMap源碼分析(JDK1.8)

HashMap源碼分析(JDK1.8)

mage -s ret 增刪 函數 tno png log 唯一性

一、HashMap簡介

  HashMap是一種基於數組+鏈表+紅黑樹的數據結構,其中紅黑樹部分在JDK1.8後引入,當鏈表長度大於8的時候轉換為紅黑樹。

  HashMap繼承於AbstractMap(Map的骨架實現類),實現Map接口。

  HashMap因為采用hashCode的值存儲,所以性能一般情況下為O(1)。

  HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。

  HashMap線程不安全,如在多線程環境下可以使用Collections工具類將其轉換為線程安全,也可使用JDK提供的CocurrentHashMap,不建議使用HashTable,理由是並發性能一般。

二、HashMap存儲結構分析

  技術分享

  HashMap用哈希表存儲元素,其中采用鏈地址法解決沖突。

  Node<K,V>是HashMap的內部實現類,實現Map.Entry<K,V>。

  源碼為:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //哈希值
        final K key; // 鍵
        V value;  //值
        Node<K,V> next; //下個結點

        Node(
int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; }
public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

  

再來看看HashMap其他的幾個屬性:

  

transient Node<K,V>[] table; //數組
transient int size; //實際存在鍵值對數
transient int modCount; //修改次數
int threshold; //所能容納的key-value對極限
final float loadFactor; //負載因子 默認為0.75不建議修改

其中table默認的構造長度是16,並且要求length是2的n次方,這是一種非常規的設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。

當數據量增大後,即便負載因子和Hash算法設計的再合理,也避免不了鏈表長度增加,如此一來就會影響HashMap的性能,所以JDK1.8開始引入了紅黑樹,利用紅黑樹高性能的增刪查改解決這一問題,這裏不討論紅黑樹。

三、HashMap的功能實現

1)構造HashMap  

  HashMap有四個構造函數:

    以下就源碼逐個分析構造函數

public HashMap(int initialCapacity, float loadFactor) { //構造時傳入指定大小以及負載因子
        if (initialCapacity < 0)    //容量大小合法性判斷
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
     //最大容量只能為MAXIMUM_CAPACTITY,2的30次方
if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
     //負載因子合法性判斷
if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //設置負載因子 this.threshold = tableSizeFor(initialCapacity); //設置閾值,當存儲數據量達到閾值,將進行擴容,擴大為原來的2倍 }

    

 public HashMap(int initialCapacity) { //指定容量大小,負載因子使用默認值0.75,調用上面的構造器
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

public HashMap() {  //無參構造器,設置負載因子為默認值
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

public HashMap(Map<? extends K, ? extends V> m) { //包含子map的構造函數
        this.loadFactor = DEFAULT_LOAD_FACTOR; //設置默認負載因子
        putMapEntries(m, false); // 將傳入的map元素逐個加入到HashMap中
    }

2)確定數組索引位置

  

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這是JDK1.8中確定數組索引的方法,h = key.hashCode() 為第一步 取hashCode值,h ^ (h >>> 16) 為第二步 高位參與運算,這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算

3)HashMap的put方法

首先是邏輯分析,put方法的處理邏輯:

①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;

②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;

③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals;

④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

看源代碼實現:

 

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
     //tab為空則新建
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//計算index,並處理null
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k;
//通過hashcode值以及equals方法判斷key是否存在,是的話直接覆蓋value
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
       //判斷該鏈是否是紅黑樹 是的話直接插入
else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
        //不是紅黑樹 是鏈表
for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null);
              //如果鏈表長度大於8轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }
            //key已經存在直接覆蓋value
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //修改次數自增 if (++size > threshold) //判斷是否要擴容 resize(); afterNodeInsertion(evict); return null; }

4)擴容機制

 Java中數組擴容是無法自動擴容的,方法是用新數組代替舊數組。融入了紅黑樹後,resize方法復雜度大大增加,這裏簡單說下1.7的處理的過程:

  傳入新的容量--引用擴容前的數組--判斷是否達到最大容量--初始化新數組--復制數據--設置table屬性引用新的數組--修改閾值

5)HashMap的get方法

  HashMap存儲元素是通過計算HashCode值確定精確索引存放的,但是不同的key是有可能出現有相同的HashCode值的,這時候他們會被存放在同一個桶中(bucket),

   然後通過equals方法來確保key的唯一性。

   因此當取出數據時,就要根據hash算法找到在數組的存儲位置,再根據equals方法從該桶中找出對應的數據。

   明白了邏輯以後看源代碼實現:

  

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
          //總是檢驗第一個看是否命中 ((k = first.key) == key || (key != null && key.equals(k)))) return first;
        //未命中
if ((e = first.next) != null) {
          //在紅黑樹中獲取
if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { //在鏈表中獲取 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

  

針對HashMap的學習主要理解它的存儲結構,工作原理,工作原理可以通過put與get方法去理解,期間可能產生擴容;另外要明白hashcode和equals兩個方法在HashMap中

的作用。此外還有一些常用的方法containsKey,clear,remove,可以查看源碼學習。

  

HashMap源碼分析(JDK1.8)