1. 程式人生 > >【集合框架】之深入分析HashMap

【集合框架】之深入分析HashMap

提出並解決問題如下:

問題1:初始容量為什麼是16,為什麼必須是2的冪?

問題2: hash方法為什麼是無符號右移16位?

問題3:

問題4:

問題5:

HashMap

  • 非執行緒安全
  • 繼承於AbstractMap
  • 實現了Map、Cloneable、java.io.Serializable介面
  • 基於陣列+連結串列+紅黑樹

重要物件

  • DEFAULT_INITIAL_CAPACITY :預設的初始容量是16,必須是2的冪
  • MAXIMUM_CAPACITY :最大容量,1<<30
  • table:Entry[]陣列型別,每一個Entry本質上是一個單向連結串列
  • size:HashMap的大小,鍵值對的數量
  • threshold:HashMap的閾值,threshold=容量*載入因子
  • DEFAULT_LOAD_FACTOR:預設載入因子0.75
  • loadFactor:實際載入因子
  • modCount:HashMap被改變的次數,用來實現fail-fast機制
  • HashMap擴容時,將容量變為原來的2倍
  • TREEIFY_THRESHOLD :樹形和列表的閾值,預設8
  • UNTREEIFY_THRESHOLD :樹形轉換回鏈式的閾值,預設6
  • MIN_TREEIFY_CAPACITY :雜湊表的最小樹形化容量,預設64

問題1:初始容量為什麼是16,為什麼必須是2的冪?

  • 指定為16是從效能考慮。避免重複計算
  • 陣列的長度總是 2 的冪,使Hash雜湊更均勻
    HashMaphash函式通過 hash & (table.length - 1) 來得到該物件的index.僅與hash值的低n位有關
    因為使用的掩碼是2的次冪,高於掩碼的位組成的雜湊集合總是衝突,所以把高位移到低位。
    混合原始雜湊碼的高位和低位,以此來加大低位的隨機性。

構造方法

可指定初始容量和負載因子,或從其他Map初始化

核心方法

hash()
key的hash值高16位不變,低16位與高16位異或作為key的最終hash值

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

問題2: hash方法為什麼是無符號右移16位?

設計者權衡了speed, utility, and quality

在JDK1.7中,使用四次移位

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

tableSizeFor()

返回一個比給定整數大且最接近的2的冪次方整數

問題3: tableSizeFor如何實現?

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

先取n-1,五次右移後做運算
超過最大值取最大值,否則取n-1
作為閾值threshold

  • 第一次右移1位:最高位前2位置1
  • 第二次右移2位:最高位前4位置1
  • 第三次右移4位:最高位前8位置1
  • 第四次右移8位:最高位前16位置1
  • 第五次右移8位:最高位前32位置1

put

通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去連結串列或樹中去查詢對應的節點.

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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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) {
                    // 插入到連結串列的最後面(Java7 是插入到連結串列的最前面)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在該連結串列中找到了"相等"的 key(== 或 equals)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e!=null 說明存在舊值的key與要插入的key"相等"
            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;
    }

具體過程

如果table為空或大小為0,觸發resize,用預設值初始化
(n - 1) & hash找到buckets,為空則在此插入

if ((tab = table) == null || (n = tab.length) == 0)
      n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null);

如果產生碰撞,取出該節點p

如果hash相等且(key ==equal),即就是要找的節點e

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

如果不是要找的節點,就判斷p是紅黑是還是連結串列節點,呼叫不同的插入方法

如果插入後超過8個,會觸發 treeifyBin將連結串列轉換為紅黑樹
如果在該連結串列中找到了相等的 key(== 或 equals)

如果e!=null 說明存在舊值的key與要插入的key 相等
進行 “值覆蓋”,然後返回舊值

判斷閾值,決定是否擴容

未完待續