環境:jdk1.8
構造函數
首先我們看下HashMap構造函數,以及默認容量DEFAULT_INITIAL_CAPACITY設置,指定初始化容量的構造函數中對初始化容量做了2的冪處理,例如:指定17,處理後會變成32(向上取冪)。默認容量16也是2的冪,並且註釋中寫明瞭必須爲2的冪。
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
...
public HashMap(int initialCapacity, float loadFactor) {
...
this.threshold = tableSizeFor(initialCapacity);
}
很多朋友可能會感覺到奇怪,爲什麼必須要是2的冪呢?我們繼續看它的源碼來挖掘原因
put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
第一步是取key的hash值,我們知道一個hash算法的好壞主要看其hash後元素分佈的均勻性,越是均勻,hash衝突也就是越少。我們看下hashmap如何實現的hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- hashCode()函數是Object提供的native函數,調用系統函數返回一個內存地址轉換而來的int值
- h ^ (h >>> 16),該函數什麼意思呢?首先h無符號右移16位,剛好32位的一半,然後h與移位的結果做異或運算。異或算法也就是同假異真或不進行進位的加法。右移16位後高16位全部補0,與原高位16位異或結果不會改變原高16位。低16位與移位後的高16位異或運算
第二步其實就是將Object提供的hashCode返回值的低16位變成它的低16位與高16位異或運算後的值。這麼做有什麼好處呢?
如果低16位的兩個位爲00,高16位對應位置的兩個位異或運算後兩個位相同的概率是2/4(00或11),也就是有2/4的概率不同,異或後結果不是00或11的概率有2/4(01或10),顯然對於一個兩位的二進制01比00更均勻。所有場景如下表。可以看出異或運算後結果更加傾向均勻分佈。正式我們期待的結果
兩位長度二進制組合 | 異或運算後兩個位數字相同的概率 | 異或運算後兩個位數字不相同的概率 |
---|---|---|
00 | 2/4 | 2/4 |
01 | 1/4 | 3/4 |
10 | 1/4 | 3/4 |
11 | 2/4 | 2/4 |
第二步putVal
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);
...
}
tab就是map的hash表,根據hash判斷表中是否存在數據,也就是根據hash轉換爲hash表的下標,然後判斷下標處是否有數據即可。hash值不能直接作爲下標嗎?爲什麼要轉換?因爲我們的hash表的大小默認是16,通常指定hash表的大小也不會是Integer最大值,因爲我們的hash函數返回的是一個int值,那麼就會存在下標越界問題,如何處理下標越界問題呢?直接使用hash值對hash表的大小取餘數即可。那麼我們看下HashMap如何處理的?顯然不是直接用的取餘算法,而是按位與運算:hash表大小-1 & hash值。
命題:X % (2^n) = [(2^n) - 1] & X
上面我們看到HashMap中的處理:(hash表大小-1) & hash值等同於取餘運算
首先,我們回憶下2進制換算10進制的方法:
12的二進制表示:1100=12^3 + 12^2 + 02^1 + 02^0
取餘數算法:
13%3=1:3+3+3+3+1
13%11=2:11+2
13%8=5:8+5
13%2=1:2+2+2+2+2+2+1
13轉爲2進制的另一種表示方法:12^3 + 12^2 + 02^1 + 12^0
對2取餘,2轉爲2進制的另一種表示方法:12^1 + 020=1*20
對4取餘,4轉爲2進制的另一種表示方法:12^2 + 02^1 + 020=0*21 + 12^0
對8取餘,8轉爲2進制的另一種表示方法:12^3 + 02^2 + 02^1 + 020=1*22 + 02^1 + 12^0
…
寫程序驗證可以得出結論,即:X∈Integer,2^n∈Integer
結論
任意10進制數字對2的冪求模運算,等於10進制轉爲2進制後第(2^n) - 1右邊的所有位轉成10進制後的數字。
如何獲取10進制轉二進制後,第(2^n) - 1位的右邊的所有位。沒錯就是對(2^n) - 1按位與運算。如下表:
2^n | 13的二進制 | [(2^n)-1]的二進制 | 按位與遠算 | 餘數 | |
---|---|---|---|---|---|
n=1 | 2 | 1101 | 0001 | 0001 | 1 |
n=2 | 4 | 1101 | 0011 | 0001 | 1 |
n=3 | 8 | 1101 | 0111 | 0101 | 5 |
性能比較
小於等於1千萬次,模運算性能高於位運算
大於等於1億次,模運算性能低於位運算
位運算耗時 | 模運算耗時 | 模運算耗時/位運算耗時 | 位運算耗時/模運算耗時 | ||
---|---|---|---|---|---|
100000次位運算耗時 | 13 | 100000次模運算耗時 | 8 | 0.615384615 | 1.625 |
1000000次位運算耗時 | 86 | 1000000次模運算耗時 | 59 | 0.686046512 | 1.457627119 |
10000000次位運算耗時 | 628 | 10000000次模運算耗時 | 559 | 0.890127389 | 1.123434705 |
100000000次位運算耗時 | 5125 | 100000000次模運算耗時 | 5830 | 1.137560976 | 0.879073756 |
1000000000次位運算耗時 | 60795 | 1000000000次模運算耗時 | 70724 | 1.163319352 | 0.859609185 |
性能比較代碼
package com.mytest;
import java.util.Random;
/**
* @author 會灰翔的灰機
* @date 2020/3/15
*/
public class BitAndModular {
public static void main(String[] args) {
bit();
modular();
}
public static void bit() {
int power = 10;
int number = 10000;
while(true) {
number *= power;
if (number <= 1000000000) {
long start = System.currentTimeMillis();
int result = 0;
for (int j = 1; j < number; j++) {
result = (16777216 - 1) & new Object().hashCode();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%s次位運算耗時\t%s", number, (end - start)));
} else {
break;
}
}
}
public static void modular() {
int power = 10;
int number = 10000;
for(;;) {
number *= power;
if (number <= 1000000000) {
long start = System.currentTimeMillis();
int result = 0;
for (int j = 1; j < number; j++) {
result = new Object().hashCode() % 16777216;
}
long end = System.currentTimeMillis();
System.out.println(String.format("%s次模運算耗時\t%s", number, (end - start)));
} else {
break;
}
}
}
}