天天看點

HashMap源碼分析

HashMap的曆史

HashMap最早是在jdk1.2中開始出現的,一直到jdk1.7一直沒有太大的變化。

但是到了jdk1.8突然進行了一個很大的改動。其中一個最顯著的改動就是:

之前jdk1.7的存儲結構是數組+連結清單,到了jdk1.8變成了數組+連結清單+紅黑樹。

另外,HashMap是非線程安全的,進行增删改操作的時候,是不能保證資料的一緻性的。

HashMap源碼分析

紅黑樹是一個自平衡的二叉查找樹,也就是說紅黑樹的查找效率是非常的高,查找效率會從連結清單的o(n)降低為o(logn)。

HashMap名詞介紹

名稱 用途
initialCapacity HashMap 初始容量
loadFactor 負載因子
threshold 目前 HashMap 所能容納鍵值對數量的最大值,超過這個值,則需擴容

負載因子(loadFactor)

對于 HashMap 來說,負載因子是一個很重要的參數,該參數反應了 HashMap 桶數組的使用情況。

通過調節負載因子,可使 HashMap 時間和空間複雜度上有不同的表現。

當我們調低負載因子時,HashMap 所能容納的鍵值對數量變少。

擴容時,重新将鍵值對存儲新的桶數組裡,鍵的鍵之間産生的碰撞會下降,連結清單長度變短。此時,HashMap 的增删改查等操作的效率将會變高,這裡是典型的拿空間換時間。

相反,如果增加負載因子(負載因子可以大于1),HashMap 所能容納的鍵值對數量變多,空間使用率高,但碰撞率也高。這意味着連結清單長度變長,效率也随之降低,這種情況是拿時間換空間。

至于負載因子怎麼調節,這個看使用場景了。一般情況下,我們用預設值就可以了。

Node[] table

即哈希桶數組 ,Node是HashMap的一個内部類,實作了Map.Entry接口,本質是就是一個映射(鍵值對)。

Node[] table的初始化長度length(預設值是16),

Load factor為負載因子(預設值是0.75),

threshold是HashMap所能容納的最大資料量的Node(鍵值對)個數。

threshold = length * Load factor。

也就是說,在數組定義好長度之後,負載因子越大,所能容納的鍵值對個數越多。

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;        }         }           

源碼解析

初始化 threshold 的方法

/**      * Returns a power of two size for the given target capacity.      */     static final int tableSizeFor(int cap) {         int n = cap - 1;         n |= n >>> 1;         n |= n >>> 2;         n |= n >>> 4;         n |= n >>> 8;         n |= n >>> 16;         return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;     }           

總結起來就一句話:找到大于或等于 cap 的最小2的幂。

put(K key, V value)

public V put(K key, V value) {             return putVal(hash(key), key, value, false, true);         }           
也就是說,put方法其實調用的是putVal方法。putVal方法有5個參數:     (1)第一個參數hash:調用了hash方法計算hash值     (2)第二個參數key:就是我們傳入的key值,也就是例子中的張三     (3)第三個參數value:就是我們傳入的value值,也就是例子中的20     (4)第四個參數onlyIfAbsent:也就是當鍵相同時,不修改已存在的值     (5)第五個參數evict :如果為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;             //第一部分             //如果table尚未初始化,則此處進行初始化數組,并指派初始容量,重新計算門檻值             if ((tab = table) == null || (n = tab.length) == 0)                 n = (tab = resize()).length;             //第二部分             //判斷hash值,不沖突,new一個Node插入進去             if ((p = tab[i = (n - 1) & hash]) == null)                 tab[i] = newNode(hash, key, value, null);             //第三部分             //處理沖突,如果通過hash找到的位置有資料,發生碰撞             else {                 Node<K,V> e;                  K k;                 //第三部分--A                 //p是目前節點,如果需要插入的key和目前hash值指定下标的key一樣,一樣的直接替換舊值e                 if (p.hash == hash &&                     ((k = p.key) == key || (key != null && key.equals(k))))                     e = p;                 //第三部分--B                 //如果此時桶中資料類型為 treeNode,使用紅黑樹進行插入                 else if (p instanceof TreeNode)                     e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);                 //第三部分--C                 //此時桶中資料類型為連結清單                 else {                     //看這裡的 .next,如果是數組,就會進去周遊                     //沒有 就newNode                //化樹的門檻值,TREEIFY_THRESHOLD,是以不存在并且在連結清單末尾插入元素的時候                     for (int binCount = 0; ; ++binCount) {                         if ((e = p.next) == null) {                             p.next = newNode(hash, key, value, null);                             //如果連結清單過長,達到樹化門檻值,将連結清單轉化成紅黑樹                             if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                                 treeifyBin(tab, hash);                             break;                         }                         //如果連結清單中有新插入的節點位置資料不為空,則此時e 指派為節點的值,跳出循環                         if (e.hash == hash &&                             ((k = e.key) == key || (key != null && key.equals(k))))                             break;                         p = e;                     }                 }                 //經過上面的循環後,如果e不為空,則說明上面插入的值已經存在于目前的hashMap中,                 //那麼更新指定位置的鍵值對                 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;         }           
HashMap源碼分析
HashMap源碼分析

代碼看,put方法分為三種情況:

  • table尚未初始化,對資料進行初始化
  • table已經初始化,且通過hash算法找到下标所在的位置資料為空,直接将資料存放到指定位置
  • table已經初始化,且通過hash算法找到下标所在的位置資料不為空,發生hash沖突(碰撞),發生碰撞後,會執行以下操作:
    • 判斷插入的key如果等于目前位置的key的話,将 e 指向該鍵值對
    • 如果此時桶中資料類型為 treeNode,使用紅黑樹進行插入
    • 如果是連結清單,則進行循環判斷, 如果連結清單中包含該節點,跳出循環,如果連結清單中不包含該節點,則把該節點插入到連結清單末尾,同時,如果連結清單長度超過樹化門檻值(TREEIFY_THRESHOLD)且table容量超過最小樹化容量(MIN_TREEIFY_CAPACITY),則進行連結清單轉紅黑樹(由于table容量越小,越容易發生hash沖突,是以在table容量<MIN_TREEIFY_CAPACITY 的時候,如果連結清單長度>TREEIFY_THRESHOLD,會優先選擇擴容,否則會進行連結清單轉紅黑樹操作)

擴容前的小知識

怎麼計算Hash值?

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

​ 與16異或的位運算。

​ 在資料結構中,我們處理hash沖突常使用的方法有:開發定址法、再哈希法、鍊位址法、建立公共溢出區。而hashMap中處理hash沖突的方法就是鍊位址法。

​ “鍊”,很容易想到,因為常用删除修改等操作,是以效率更高。

​ initialCapacity初始容量(你輸入的最接近2的N次幂的數字),loadFactor負載因子(0.75,泊松分布)。

/* ---------------- Public operations -------------- */         /**          * Constructs an empty <tt>HashMap</tt> with the specified initial          * capacity and load factor.          *          * @param  initialCapacity the initial capacity          * @param  loadFactor      the load factor          * @throws IllegalArgumentException if the initial capacity is negative          *         or the load factor is nonpositive          */         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;             this.threshold = tableSizeFor(initialCapacity);         }         /**          * Constructs an empty <tt>HashMap</tt> with the specified initial          * capacity and the default load factor (0.75).          *          * @param  initialCapacity the initial capacity.          * @throws IllegalArgumentException if the initial capacity is negative.          */         public HashMap(int initialCapacity) {             this(initialCapacity, DEFAULT_LOAD_FACTOR);         }         /**          * Constructs an empty <tt>HashMap</tt> with the default initial capacity          * (16) and the default load factor (0.75).          */         public HashMap() {             this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted         }         /**          * Constructs a new <tt>HashMap</tt> with the same mappings as the          * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with          * default load factor (0.75) and an initial capacity sufficient to          * hold the mappings in the specified <tt>Map</tt>.          *          * @param   m the map whose mappings are to be placed in this map          * @throws  NullPointerException if the specified map is null          */         public HashMap(Map<? extends K, ? extends V> m) {             this.loadFactor = DEFAULT_LOAD_FACTOR;             putMapEntries(m, false);         }           

擴容 resize()

/**          * Initializes or doubles table size.  If null, allocates in          * accord with initial capacity target held in field threshold.          * Otherwise, because we are using power-of-two expansion, the          * elements from each bin must either stay at same index, or move          * with a power of two offset in the new table.          *          * @return the table          */     //初始化或加倍表大小。如果為空,則根據字段門檻值中持有的初始容量目标進行配置設定。否則,因為我們使用的是 2 的幂擴充,是以每個 bin 中的元素必須保持相同的索引,或者在新表中以 2 的幂的偏移量移動。         final Node<K,V>[] resize() {             Node<K,V>[] oldTab = table;             int oldCap = (oldTab == null) ? 0 : oldTab.length;             int oldThr = threshold;		//threshold 臨界點             int newCap, newThr = 0;             //第一部分,擴容             //如果超過了直接将門檻值設定為整數的最大值,沒有,則,通過移位操作擴大2倍。             if (oldCap > 0) {                 //如果舊的容量已近達到最大值,則不再擴容,門檻值直接設定為最大值                 if (oldCap >= MAXIMUM_CAPACITY) {                     threshold = Integer.MAX_VALUE;                     return oldTab;                 }                 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                          oldCap >= DEFAULT_INITIAL_CAPACITY)                     //如果舊的容量不小于預設的初始容量,則進行擴容,容量擴張為原來的二倍                     newThr = oldThr << 1; // double threshold             }             //第二部分             //如果初始化過了,就使用已經有的門檻值             //不然,就重新初始化一個             else if (oldThr > 0) // initial capacity was placed in threshold                 newCap = oldThr;             //threshold 和 table 皆未初始化情況,此處即為首次進行初始化             //也就在此處解釋了構造方法中沒有對threshold 和 初始容量進行指派的問題             else {               // zero initial threshold signifies using defaults                 //如果門檻值為零,表示使用預設的初始化值                 //這種情況在調用無參構造的時候會出現,此時使用預設的容量和門檻值                 newCap = DEFAULT_INITIAL_CAPACITY;                 //此處門檻值即為 threshold=initialCapacity*loadFactor                 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);             }             // newThr 為 0 時,按門檻值計算公式進行計算,容量*負載因子             if (newThr == 0) {            float ft = (float)newCap * loadFactor;                 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                           (int)ft : Integer.MAX_VALUE);             }              //更新門檻值             threshold = newThr;             //更新數組桶             @SuppressWarnings({"rawtypes","unchecked"})             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];             table = newTab;             //第三部分             //将舊的資料遷移到新的數組             //如果之前的數組桶裡面已經存在資料,由于table容量發生變化,hash值也會發生變化,需要重新計算下标             if (oldTab != null) {                 //周遊舊的資料                 for (int j = 0; j < oldCap; ++j) {                     Node<K,V> e;                     //如果指定下标下有資料                     if ((e = oldTab[j]) != null) {                         //将指定下标資料置空                         oldTab[j] = null;                         //指定下标隻有一個資料                         if (e.next == null)                             //直接将資料存放到新計算的hash值下标下                             newTab[e.hash & (newCap - 1)] = e;                         //如果是TreeNode資料結構                         else if (e instanceof TreeNode)                             ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                         //對于連結清單,資料結構                         else { // preserve order                             //如果是連結清單,重新計算hash值,根據新的下标重新分組                             Node<K,V> loHead = null, loTail = null;                             Node<K,V> hiHead = null, hiTail = null;                             Node<K,V> next;                             //逐個轉移數組                             do {                                 next = e.next;                                 if ((e.hash & oldCap) == 0) {                                     if (loTail == null)                                         loHead = e;                                     else                                         loTail.next = e;                                     loTail = e;                                 }                                 else {                                     if (hiTail == null)                                         hiHead = e;                                     else                                         hiTail.next = e;                                     hiTail = e;                                 }                             } while ((e = next) != null);                             if (loTail != null) {                                 loTail.next = null;                                 newTab[j] = loHead;                             }                             if (hiTail != null) {                                 hiTail.next = null;                                 newTab[j + oldCap] = hiHead;                             }                         }                     }                 }             }             return newTab;         }           
HashMap源碼分析

resize分為以下幾步:

  • 首先先判斷目前table是否進行過初始化,如果沒有進行過初始化,此處就解決了調用無參構造方法時候,threshold和initialCapacity 未初始化的問題,如果已經初始化過了,則進行擴容,容量為原來的二倍
  • 擴容後建立新的table,并對所有的資料進行周遊
    • 如果新計算的位置資料為空,則直接插入
    • 如果新計算的位置為連結清單,則通過hash算法重新計算下标,對連結清單進行分組
    • 如果是紅黑樹,則需要進行拆分操作

get方法,查找

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) {                 //根據hash算法找到對應位置的第一個資料,如果是指定的key,則直接傳回                 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;         }           

hash源碼

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

這段代碼叫做擾動函數,也是hashMap中的hash運算,主要分為下面幾步:

  • key.hashCode(),擷取key的hashCode值,如果不進行重寫的話傳回的是根據記憶體位址得到的一個int值
  • key.hashCode() 擷取到的hashcode無符号右移16位并和元hashCode進行^ ,這樣做的目的是為了讓高位與低進行混合,讓兩者都參與運算,以便讓hash值分布更加均勻

取模運算 (n - 1) & hash

在hashMap的代碼中,在很多地方都會看到類似的代碼:

first = tab[(n - 1) & hash])           

hash算法中,為了使元素分布的更加均勻,很多都會使用取模運算,

在hashMap中并沒有使用hash%n這樣進行取模運算,而是使用(n - 1) & hash進行代替,

原因是在計算機中,&的效率要遠高于%。

需要注意的是,     隻有容量為2的n次幂的時候,(n - 1) & hash 才能等效hash%n,     這也是hashMap 初始化初始容量時,無論傳入任何值,都會通過tableSizeFor(int cap) 方法轉化成2的n次幂的原因,     這種巧妙的設計真的很令人驚歎。           

有興趣的可以看一下一位大佬寫的文章:由HashMap雜湊演算法引出的求餘%和與運算&轉換問題

remove方法,删除

public V remove(Object key) {             Node<K,V> e;             return (e = removeNode(hash(key), key, null, false, true)) == null ?                 null : e.value;         }         final Node<K,V> removeNode(int hash, Object key, Object value,                                    boolean matchValue, boolean movable) {             Node<K,V>[] tab; Node<K,V> p; int n, index;             //根據key和key的hash值,查找到對應的元素             if ((tab = table) != null && (n = tab.length) > 0 &&                 (p = tab[index = (n - 1) & hash]) != null) {                 Node<K,V> node = null, e; K k; V v;                 if (p.hash == hash &&                     ((k = p.key) == key || (key != null && key.equals(k))))                     node = p;                 else if ((e = p.next) != null) {                     if (p instanceof TreeNode)                         node = ((TreeNode<K,V>)p).getTreeNode(hash, key);                     else {                         do {                             if (e.hash == hash &&                                 ((k = e.key) == key ||                                  (key != null && key.equals(k)))) {                                 node = e;                                 break;                             }                             p = e;                         } while ((e = e.next) != null);                     }                 }                 //如果查找的了元素node,移除即可                 if (node != null && (!matchValue || (v = node.value) == value ||                                      (value != null && value.equals(v)))) {                     //如果是TreeNode,通過樹進行移除                     if (node instanceof TreeNode)                         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);                     //如果是第一個節點,移除第一個節點,将index下标的位置指向第二個節點                     else if (node == p)                         tab[index] = node.next;                     else                         //如果不是連結清單的第一個節點,則移除該節點                         p.next = node.next;                     ++modCount;                     --size;                     afterNodeRemoval(node);                     return node;                 }             }             return null;         }           

連結清單樹化

static final int TREEIFY_THRESHOLD = 8;     /**      * 當桶數組容量小于該值時,優先進行擴容,而不是樹化      */     static final int MIN_TREEIFY_CAPACITY = 64;     static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {         TreeNode<K,V> parent;  // red-black tree links         TreeNode<K,V> left;         TreeNode<K,V> right;         TreeNode<K,V> prev;    // needed to unlink next upon deletion         boolean red;         TreeNode(int hash, K key, V val, Node<K,V> next) {             super(hash, key, val, next);         }     }     /**      * 将普通節點連結清單轉換成樹形節點連結清單      */     final void treeifyBin(Node<K,V>[] tab, int hash) {         int n, index;          Node<K,V> e;         // 桶數組容量小于 MIN_TREEIFY_CAPACITY,優先進行擴容而不是樹化         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)             resize();         else if ((e = tab[index = (n - 1) & hash]) != null) {             // hd 為頭節點(head),tl 為尾節點(tail)             TreeNode<K,V> hd = null, tl = null;             do {                 // 将普通節點替換成樹形節點                 TreeNode<K,V> p = replacementTreeNode(e, null);                 if (tl == null)                     hd = p;                 else {                     p.prev = tl;                     tl.next = p;                 }                 tl = p;             } while ((e = e.next) != null);  // 将普通連結清單轉成由樹形節點連結清單             if ((tab[index] = hd) != null)                 // 将樹形連結清單轉換成紅黑樹                 hd.treeify(tab);         }     }     TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {         return new TreeNode<>(p.hash, p.key, p.value, next);     }           

在擴容過程中,樹化要滿足兩個條件:

  1. 連結清單長度大于等于 TREEIFY_THRESHOLD
  2. 桶數組容量大于等于 MIN_TREEIFY_CAPACITY

紅黑樹鍊化與拆分

鍊化

final Node<K,V> untreeify(HashMap<K,V> map) {         Node<K,V> hd = null, tl = null;         // 周遊 TreeNode 連結清單,并用 Node 替換         for (Node<K,V> q = this; q != null; q = q.next) {             // 替換節點類型             Node<K,V> p = map.replacementNode(q, null);             if (tl == null)                 hd = p;             else                 tl.next = p;             tl = p;         }         return hd;     }     Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {         return new Node<>(p.hash, p.key, p.value, next);     }           

拆分

// 紅黑樹轉連結清單門檻值     static final int UNTREEIFY_THRESHOLD = 6;     final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {         TreeNode<K,V> b = this;         // Relink into lo and hi lists, preserving order         TreeNode<K,V> loHead = null, loTail = null;         TreeNode<K,V> hiHead = null, hiTail = null;         int lc = 0, hc = 0;         /*           * 紅黑樹節點仍然保留了 next 引用,故仍可以按連結清單方式周遊紅黑樹。          * 下面的循環是對紅黑樹節點進行分組,與上面類似          */         for (TreeNode<K,V> e = b, next; e != null; e = next) {             next = (TreeNode<K,V>)e.next;             e.next = null;             if ((e.hash & bit) == 0) {                 if ((e.prev = loTail) == null)                     loHead = e;                 else                     loTail.next = e;                 loTail = e;                 ++lc;             }             else {                 if ((e.prev = hiTail) == null)                     hiHead = e;                 else                     hiTail.next = e;                 hiTail = e;                 ++hc;             }         }         if (loHead != null) {             // 如果 loHead 不為空,且連結清單長度小于等于 6,則将紅黑樹轉成連結清單             if (lc <= UNTREEIFY_THRESHOLD)                 tab[index] = loHead.untreeify(map);             else {                 tab[index] = loHead;                 /*                   * hiHead == null 時,表明擴容後,                  * 所有節點仍在原位置,樹結構不變,無需重新樹化                  */                 if (hiHead != null)                      loHead.treeify(tab);             }         }         // 與上面類似         if (hiHead != null) {             if (hc <= UNTREEIFY_THRESHOLD)                 tab[index + bit] = hiHead.untreeify(map);             else {                 tab[index + bit] = hiHead;                 if (loHead != null)                     hiHead.treeify(tab);             }         }     }           

在将普通連結清單轉成紅黑樹時,HashMap 通過兩個額外的引用 next 和 prev 保留了原連結清單的節點順序。

這樣再對紅黑樹進行重新映射時,完全可以按照映射連結清單的方式進行。這樣就避免了将紅黑樹轉成連結清單後再進行映射,無形中提高了效率。

從源碼上可以看得出,重新映射紅黑樹的邏輯和重新映射連結清單的邏輯基本一緻。不同的地方在于,重新映射後,會将紅黑樹拆分成兩條由 TreeNode 組成的連結清單。

如果連結清單長度小于 UNTREEIFY_THRESHOLD,則将連結清單轉換成普通連結清單。否則根據條件重新将 TreeNode 連結清單樹化。

HashMap源碼分析

被 transient 所修飾 table 變量

其實這個關鍵字的作用很好了解,

就是簡單的一句話:将不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會被序列化。

transient Node<K,V>[] table;         /**          * Holds cached entrySet(). Note that AbstractMap fields are used          * for keySet() and values().          */         transient Set<Map.Entry<K,V>> entrySet;         /**          * The number of key-value mappings contained in this map.          */         transient int size;         /**          * The number of times this HashMap has been structurally modified          * Structural modifications are those that change the number of mappings in          * the HashMap or otherwise modify its internal structure (e.g.,          * rehash).  This field is used to make iterators on Collection-views of          * the HashMap fail-fast.  (See ConcurrentModificationException).          */         transient int modCount;           

HashMap 并沒有使用預設的序列化機制,而是通過實作

readObject/writeObject

兩個方法自定義了序列化的内容。

序列化 talbe 存在着兩個問題:

  1. table 多數情況下是無法被存滿的,序列化未使用的部分,浪費空間
  2. 同一個鍵值對在不同 JVM 下,所處的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能會發生錯誤。

第二個問題解釋:HashMap 的

get/put/remove

等方法第一步就是根據 hash 找到鍵所在的桶位置,

但如果鍵沒有覆寫 hashCode 方法,計算 hash 時最終調用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能會有不同的實作,産生的 hash 可能也是不一樣的。也就是說同一個鍵在不同平台下可能會産生不同的 hash,此時再對在同一個 table 繼續操作,就會出現問題。

總結

  • HashMap 底層資料結構在JDK1.7之前是由數組+連結清單組成的,1.8之後又加入了紅黑樹;連結清單長度小于8的時候,發生Hash沖突後會增加連結清單的長度,當連結清單長度大于8的時候,會先判讀數組的容量,如果容量小于64會先擴容(原因是數組容量越小,越容易發生碰撞,是以當容量過小的時候,首先要考慮的是擴容),如果容量大于64,則會将連結清單轉化成紅黑樹以提升效率。
  • hashMap 的容量是2的n次幂,無論在初始化的時候傳入的初始容量是多少,最終都會轉化成2的n次幂,這樣做的原因是為了在取模運算的時候可以使用&運算符,而不是%取餘,可以極大的提上效率,同時也降低hash沖突。
  • HashMap是非線程安全的,在多線程的操作下會存在異常情況(如形成閉環(1.7),1.8已修複閉環問題,但仍不安全),可以使用HashTable或者ConcurrentHashMap進行代替

HashMap這樣的知識點還有很多,一篇文章是說不完的,至于更多的玄機需要我們一起去探索。

參考連結:

HashMap源碼分析(jdk1.8,保證你能看懂) - 知乎 (zhihu.com)

Java 8系列之重新認識HashMap - 知乎 (zhihu.com)

有大量時間看這個,特别全: HashMap 源碼詳細分析(JDK1.8) - SegmentFault 思否

HashMap源碼分析 —— 一篇文章搞定HashMap面試 (juejin.cn)