HashMap中的位運算

ava 8 中 HashMap 的實現使用了不少位操做來進行優化。本文將詳細介紹每種位操做優化的原理及做用。數組


  • Java 中的位運算app

    • 位操做包含:與、或、非、異或框架

    • 移位操做包含:左移、右移、無符號右移dom

  • HashMap 中的位運算ide

    • 計算哈希桶索引源碼分析

    • hashCode方法優化測試

    • 指定初始化容量優化

    • 擴容方法裏的位運算
      ui

  • 總結回顧this


HashMap中的位運算

Java 8 中,HashMap 類使用了不少位運算來進行優化,位運算是很是高效的。下邊咱們將詳細介紹。

Java 中的位運算

位操做包含:與、或、非、異或

  • 與 &,兩個操做數中的位都是1,結果爲1,不然爲0。

    • 1 & 1 = 1

    • 0 & 1 = 0

    • 1 & 0 = 0

    • 0 & 0 = 0

  • 或 |,兩個操做數中的位只要有一個爲1,結果爲1,不然爲0。

    • 1 | 1 = 1

    • 0 | 1 = 1

    • 1 | 0 = 1

    • 0 | 0 = 0

  • 非 ~,單個操做數中的位爲0,結果爲1;若是位爲1,結果爲0。

    • ~1 = 0

    • ~0 = 1

  • 異或 ^,兩個操做數中的位相同結爲0,不然爲1。

    • 1 ^ 1 = 0

    • 0 ^ 1 = 1

    • 1 ^ 0 = 1

    • 0 ^ 0 = 0

移位操做包含:左移、右移、無符號右移

  • 左移 <<,左移 n 爲至關於乘以 2n,例如 num << 1,num 左移1位 = num * 2;num << 2,num 左移2位 = num * 4

  • 右移 >>,右移 n 爲至關於除以 2n,例如 num >> 1,num 右移1位 = num / 2;num >> 2,num 右移2位 = num / 4

  • 無符號右移 >>>,計算機中數字以補碼存儲,首位爲符號位;無符號右移,忽略符號位,左側空位補0

HashMap 中的位運算

Java 8 中 HashMap 的實現結構以下圖所示,對照結構圖咱們將分別介紹 HashMap 中的幾種位運算的實現原理以及它們的做用、優勢。

圖片

計算哈希桶索引

HashMap 的 put(key, value) 操做和 get(key) 操做,會根據 key 值計算出該 key 對應的值存放的桶的索引。計算過程以下:

  1. 計算 key 值的哈希值獲得一個正整數,hash(key) = hash

  2. 使用 hash(key) 獲得的正整數,除以桶的長度取餘,結果即爲 key 值對應 value 所在桶的索引,index = hash(key) % length

put/get操做,計算key值對應value所在哈希桶的索引的主要代碼

// table 即爲上述結構圖中存放左邊桶的數組
transient Node<K,V>[] table;

// 計算 key 值的哈希值
static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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)
       // 當 table 爲 null 或長度爲0時,初始化數組 table
       n = (tab = resize()).length;
   // tab[i = (n - 1) & hash] 的下標表達式 i = (n - 1) & hash 即爲計算哈希桶的索引
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
   else {
       省略其餘代碼
   }
   省略其餘代碼
}

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) {
       // n = tab.length,n 即爲哈希桶的長度
       // tab[(n - 1) & hash],hash 爲 key 值的哈希值,表達式 (n - 1) & hash 爲哈希桶的索引
       省略其餘代碼
   }
   return null;
}

上述代碼中,使用了與操做來代替取餘,咱們先來看結論:當 length 爲 2 的次冪時,num & (length - 1) = num % length 等式成立,使用 Java 代碼來驗證一下:

public static void main(String[] args) {
   // n次冪
   int multiple = 0;
   // 長度
   int length;
   // 不成立的次數
   int fail = 0;
   while (true) {
       length = (int) Math.pow(2, ++multiple);
       if (length >= Integer.MAX_VALUE) {
           break;
       }
       // 隨機生成一個正整數
       int num = new Random().nextInt(Integer.MAX_VALUE - 1);
       // 判斷等式是否成立
       if ((num & (length - 1)) != num % length) {
           fail++;
       } else {
           System.out.printf("2的%d次冪,length=2^%d=%d,轉換成二進制:length=%s,length-1=%s\n",
                   multiple, multiple, length, Integer.toBinaryString(length), Integer.toBinaryString(length - 1));
       }
   }
   if (fail == 0) {
       System.out.printf("當 length 爲 2 的次冪時,num & (length - 1) = num %s length 等式成立, 最大%d次冪\n",
               "%", multiple - 1);
   }
}

執行結果:

2的1次冪,length=2^1=2,轉換成二進制:length=10,length-1=1
2的2次冪,length=2^2=4,轉換成二進制:length=100,length-1=11
2的3次冪,length=2^3=8,轉換成二進制:length=1000,length-1=111
2的4次冪,length=2^4=16,轉換成二進制:length=10000,length-1=1111
2的5次冪,length=2^5=32,轉換成二進制:length=100000,length-1=11111
2的6次冪,length=2^6=64,轉換成二進制:length=1000000,length-1=111111
2的7次冪,length=2^7=128,轉換成二進制:length=10000000,length-1=1111111
2的8次冪,length=2^8=256,轉換成二進制:length=100000000,length-1=11111111
2的9次冪,length=2^9=512,轉換成二進制:length=1000000000,length-1=111111111
2的10次冪,length=2^10=1024,轉換成二進制:length=10000000000,length-1=1111111111
2的11次冪,length=2^11=2048,轉換成二進制:length=100000000000,length-1=11111111111
2的12次冪,length=2^12=4096,轉換成二進制:length=1000000000000,length-1=111111111111
2的13次冪,length=2^13=8192,轉換成二進制:length=10000000000000,length-1=1111111111111
2的14次冪,length=2^14=16384,轉換成二進制:length=100000000000000,length-1=11111111111111
2的15次冪,length=2^15=32768,轉換成二進制:length=1000000000000000,length-1=111111111111111
2的16次冪,length=2^16=65536,轉換成二進制:length=10000000000000000,length-1=1111111111111111
2的17次冪,length=2^17=131072,轉換成二進制:length=100000000000000000,length-1=11111111111111111
2的18次冪,length=2^18=262144,轉換成二進制:length=1000000000000000000,length-1=111111111111111111
2的19次冪,length=2^19=524288,轉換成二進制:length=10000000000000000000,length-1=1111111111111111111
2的20次冪,length=2^20=1048576,轉換成二進制:length=100000000000000000000,length-1=11111111111111111111
2的21次冪,length=2^21=2097152,轉換成二進制:length=1000000000000000000000,length-1=111111111111111111111
2的22次冪,length=2^22=4194304,轉換成二進制:length=10000000000000000000000,length-1=1111111111111111111111
2的23次冪,length=2^23=8388608,轉換成二進制:length=100000000000000000000000,length-1=11111111111111111111111
2的24次冪,length=2^24=16777216,轉換成二進制:length=1000000000000000000000000,length-1=111111111111111111111111
2的25次冪,length=2^25=33554432,轉換成二進制:length=10000000000000000000000000,length-1=1111111111111111111111111
2的26次冪,length=2^26=67108864,轉換成二進制:length=100000000000000000000000000,length-1=11111111111111111111111111
2的27次冪,length=2^27=134217728,轉換成二進制:length=1000000000000000000000000000,length-1=111111111111111111111111111
2的28次冪,length=2^28=268435456,轉換成二進制:length=10000000000000000000000000000,length-1=1111111111111111111111111111
2的29次冪,length=2^29=536870912,轉換成二進制:length=100000000000000000000000000000,length-1=11111111111111111111111111111
2的30次冪,length=2^30=1073741824,轉換成二進制:length=1000000000000000000000000000000,length-1=111111111111111111111111111111
當 length 爲 2 的次冪時,num & (length - 1) = num % length 等式成立, 最大30次冪

根據上述結果咱們看出,length爲2的n次冪時,轉換爲二進制,最高位爲1,其他位爲0;length-1則全部位均爲1。1和另外一個數進行操做時,結果爲另外一個數自己。

由於 length - 1 的二進制每一位均爲1,因此 length - 1 與另外一個數進行與操做時,另外一個數的高位被截取,低位爲另外一個數對應位的自己。結果範圍爲 0 ~ length - 1,和取餘操做結果相等。

那麼桶數爲何必須是2的次冪?好比當 length = 15 時,轉換爲二進制爲 1111,length - 1 = 1110。length - 1 的二進制數最後一位爲0,所以它與任何數進行操做的結果,最後一位也必然是0,也即結果只能是偶數,不多是單數,這樣的話單數桶的空間就浪費掉了。同理:length = 12,二進制爲1100,length - 1 的二進制則爲 1011,那麼它與任何數進行操做的結果,右邊第3位必然是0,這樣一樣會浪費一些桶空間。

綜上所述,當 length 爲 2 的次冪時,num & (length - 1) = num % length 等式成立,而且它有以下特色:

  • 位運算快於取餘運算

  • length 爲 2 的次冪時,0 ~ length - 1 範圍內的數都有機會成爲結果,不會形成桶空間浪費

hashCode方法優化

上述代碼中計算哈希值方法中有一個無符號右移異或操做:^ (h >>> 16),它的做用是什麼?

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

無符號右移異或操做的主要目的是爲了讓生成的哈希值儘可能均勻。

計算哈希桶索引表達式:hash & (length - 1),一般哈希桶數不會特別大,絕大部分都在 0 ~ 216 這個區間範圍內,也便是小於 65536。所以哈希結果值 hash 再和 length - 1 進行操做時,hash 的高 16 位部分被直接捨得掉了,未參與計算。

那麼如何讓 hashCode() 結果的高 16 位部分也參與運算從而讓獲得的桶索引更加散列、更加均勻?能夠經過讓 hashCode() 結果再和它的高16位進行異或操做,這樣hashCode()結果的低16位和哈希結果的全部位都有了關聯。當 hash & (length - 1) 表達式中 length 小於 65536 時,結果就更加散列。爲何使用異或操做?與 & 操做和或 | 操做的結果更偏向於 0 或者 1,而異或的結果 0 和 1 有均等的機會。

如何實現 hashCode() 結果再和它的高16位異或操做?

  • h >>> 16,將 hashCode() 結果無符號右移,所得結果高16位移到低16位,而高16位都變爲0

  • (h = key.hashCode()) ^ (h >>> 16),再將 hashCode() 結果和無符號右移的結果進行異或

這樣所得結果的低16位就和 hashCode() 的全部位相關。當再進行 hash & (length - 1) 運算,length 小於 65536 時,結果就更加散列。

hash & (length - 1),當 length = 2n 時,hash & (length - 1) 的結果和 hash 值的低 n 位相關。

指定初始化容量

咱們知道,在構造 HashMap 時,能夠指定 HashMap 的初始容量,即桶數。而桶數必須是2的次冪,所以當咱們傳了一個非2的次冪的參數2時,計算離傳入參數最近的2的次冪做爲桶數。(注:2的次冪指的是2的整數次冪

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

HashMap 是經過 tableSizeFor 方法來計算離輸入參數最近的2的次冪。tableSizeFor 方法中使用了5次無符號右移操做。

假如如今咱們有一個二進制數 1xxxxx,x 多是 0 或者 1。咱們來按照上述代碼進行無符號右移操做:

1xxxxx |= 1xxxxx >>> 1

   1xxxxx
|   01xxxx,1xxxxx無符號右移1位的結果
=   11xxxx,或操做結果

從上述結果看出,無符號右移1位而後和原數進行操做,所得結果將最高2位變成1。咱們再將結果 11xxxx 繼續進行操做。

11xxxx |= 11xxxx >>> 2

   11xxxx
|   0011xx,11xxxx無符號右移2位的結果
=   1111xx,或操做結果

再進行 無符號右移2位而後和原數進行操做,所得結果將最高4位變成1。咱們再將結果 1111xx 繼續進行操做。

1111xx |= 1111xx >>> 4

   1111xx
|   000011,1111xx無符號右移4位的結果
=   111111,或操做結果

再進行 無符號右移4位而後和原數進行操做,所得結果將最高6位變成1。咱們再將結果 111111 繼續進行操做。

111111 |= 111111 >>> 8

   111111
|   000000,111111無符號右移8位的結果
=   111111,或操做結果

再進行 無符號右移8位而後和原數進行操做,所得結果不變,最高6位仍是1。咱們再將 111111 繼續進行操做。

111111 |= 111111 >>> 16

   111111
|   000000,111111無符號右移16位的結果
=   111111,或操做結果

再進行 無符號右移16位而後和原數進行操做,所得結果不變,最高6位仍是1。

從上述移位和或操做過程,咱們看出,每次無符號右移而後再和原數進行或操做,所得結果保證了最高 n * 2 位都爲1,其中 n 是無符號右移的位數。

爲何無符號右移 124816位並進行操做後就結束了?由於 int 爲 32 位數。這樣反覆操做後,就保證了原數最高位後面都變成了1。

二進制數,所有位都爲1,再加1後,就變成了最高位爲1,其他位都是0,這樣的數就是2的次冪。所以 tableSizeFor 方法返回:當 n 小於最大容量 MAXIMUM_CAPACITY 時返回 n + 1。

tableSizeFor 方法中,int n = cap - 1,爲何要將 cap 減 1?若是不減1的話,當 cap 已是2的次冪時,無符號右移和或操做後,所得結果正好是 cap 的 2 倍。

擴容方法裏的位運算

HashMap 的 resize() 方法進行初始化或擴容操做。

final Node<K,V>[] resize() {
   Node<K,V>[] oldTab = table;
   // 舊的數組的長度(原桶數)
   int oldCap = (oldTab == null) ? 0 : oldTab.length;
   int oldThr = threshold;
   int newCap, newThr = 0;
   // 數組已經初始化了,進行擴容操做
   if (oldCap > 0) {
       // 若是已經到達最大容量,則再也不擴容
       if (oldCap >= MAXIMUM_CAPACITY) {
           // 閥值設置爲最大 Integer 值
           threshold = Integer.MAX_VALUE;
           return oldTab;
       }
       // 未到達最大容量
       // 數組容量擴大爲原來的2倍:newCap = oldCap << 1
       // 閥值擴大爲原來的2倍:newThr = oldThr << 1
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                   oldCap >= DEFAULT_INITIAL_CAPACITY)
           newThr = oldThr << 1; // double threshold
   }
   // 數組未初始化,且閥值大於0,此處閥值爲何大於0???
   // 當構造 HashMap 時,若是傳了容量參數,將根據容量參數計算的離它最近的2的次冪
   // 即數組的容量暫存在閥值變量 threshold 中,詳見構造器方法中的語句:
   // this.threshold = tableSizeFor(initialCapacity);
   else if (oldThr > 0) // initial capacity was placed in threshold
       newCap = oldThr;
   // 數組未初始化且閥值爲0,說明使用了默認構造方法進行建立對象,即 new HashMap()
   else {               // zero initial threshold signifies using defaults
       newCap = DEFAULT_INITIAL_CAPACITY;
       newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
   }
   // newCap = oldThr; 語句以後未計算閥值,因此 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;
   // 舊數組不爲 null 時表示 resize 爲擴容操做,不然爲第一次初始化數組操做
   if (oldTab != null) {
       // 循環將數組中的每一個結點並轉移到新的數組中
       for (int j = 0; j < oldCap; ++j) {
           Node<K,V> e;
           // 獲取頭結點,若是不爲空,說明該數組中存放有元素
           if ((e = oldTab[j]) != null) {
               oldTab[j] = null;
               // 頭結點 e.next == null 時,代表鏈表或紅黑樹只有一個頭結點
               if (e.next == null)
                   newTab[e.hash & (newCap - 1)] = e;
               // 若是結點爲紅黑樹結點,則紅黑樹分裂,轉移到新表中
               else if (e instanceof TreeNode)
                   ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
               // 不然爲鏈表,將鏈表中各結點原序的轉移至新表中
               else { // preserve order
                   Node<K,V> loHead = null, loTail = null;
                   Node<K,V> hiHead = null, hiTail = null;
                   Node<K,V> next;
                   do {
                       next = e.next;
                       // (e.hash & oldCap) == 0 時,鏈表所在桶的索引不變
                       if ((e.hash & oldCap) == 0) {
                           if (loTail == null)
                               loHead = e;
                           else
                               loTail.next = e;
                           loTail = e;
                       }
                       // 不然將鏈表轉移到索引爲 index + oldCap 的桶中
                       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;
}

上述代碼中,擴容操做使用了左移運算

  • newCap = oldCap << 1

  • newThr = oldThr << 1

數組容量和閥值均左移1位,表示原數乘以21,即擴容爲原來的2倍。

當桶中存放的爲鏈表,在進行鏈表的轉移時,if判斷使用了以下位操做

  • if ((e.hash & oldCap) == 0)

其中 oldCap 爲擴容前數組的長度,爲2的次冪,也即它的二進制中最高位爲1,其他位都位0。而每次擴容爲原來的2倍。

例如原容量爲 16,即 oldCap = 10000,擴容後 newCap = 32,即 newCap = 100000。計算鏈表所在數組的索引表達式 hash & (length - 1)

  • 擴容前,oldCap = 10000

    • length - 1 = oldCap - 1 = 1111

    • index = hash & (length - 1) = hash & 1111

    • 數組索引下標 index 依賴於 hash 的低 4 位

  • 擴容後,newCap = 100000

    • newLength - 1 = newCap - 1 = 11111

    • newIndex = hash & (newLength - 1) = hash & 11111

    • 新數組索引下標 newIndex 依賴於 hash 的低 5 位

在上述例子中,擴容後,新的數組索引和原索引是否相等取決於 hash 的第5位,若是第5位爲0,則新的數組索引和原索引相同;若是第5位爲1,則新的數組索引和原索引不一樣。

如何測試 hash 第5位爲0仍是爲1?由於 oldCap = 10000,恰好第5位爲1,其他位都爲0,所以 e.hash & oldCap 與操做的結果,hash 第5位爲0時,結果爲0,hash 第5位爲1時,結果爲1。

綜上所述,擴容後,鏈表的拆分分兩步:

  • 一條鏈表不須要移動,保存在原索引的桶中,包含原鏈表中知足 e.hash & oldCap == 0 條件的結點

  • 一條鏈表須要移動到索引爲 index + oldCap 的桶中,包含原鏈表中不知足 e.hash & oldCap == 0 條件的結點

總結回顧

最後,咱們來總結回顧一下 HashMap 中位操做的重點內容。

圖片圖片若是你對 JDK 源碼、Spring/MyBatis框架源碼分析感興趣。能夠關注個人公衆號【開發圈DevOps】,搜索 DevCircle 或掃描下邊二維碼進行關注。

相關文章
相關標籤/搜索