1.哈希表中尋址操作:tab[i = (n - 1) & hash]
尋址就是找到即將被put進哈希表的元素在哈希表的具體下標位置
從下面的源碼可以看出n-1代表哈希表的最大下標,hash代表key經歷散列函數的值
那麼尋址操作如何保證下標不越界呢?
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) ;
}
代入參數來驗證這段代碼是如何保證下標不越界,爲了方便封裝一個方法
public static int indexFor ( int hash, int hashTableLen) {
return ( hashTableLen- 1 ) & hash;
}
public static void main ( String[ ] args) {
int hash= "ab553cu2k" . hashCode ( ) ;
int index= indexFor ( hash, 16 ) ;
}
第一步 1127860029 轉換成二進制: 0100 0011 0011 1001 1100 0111 0011 1101
第二步 代入indexFor函數 即
0100 0011 0011 1001 1100 0111 0011 1101 &
0000 0000 0000 0000 0000 0000 0000 1111
第三步得到結果0000 0000 0000 0000 0000 0000 0000 1101 等於13
可以得出結論尋址操作就是利用按位與的特性:全部是1纔是1,來保證不會出現下標越界的情況,比如上面按位與出來的結果永遠不會超過15,如果給定的哈希表長度是16的話
2.散列函數
爲什麼不直接用key的hashcode呢?還要把key的哈希值無符號右移16位再取異或key的哈希值?
public static int hash ( Object key) {
int h;
return ( key == null) ? 0 : ( h = key. hashCode ( ) ) ^ ( h >>> 16 ) ;
}
那上面的例子來說,如果拿key的哈希值去確定該元素所在哈希表的位置,
那麼最終會調用tab[ i = ( n - 1 ) & hash] ,結果如下
key的哈希值: 0100 0011 0011 1001 1100 0111 0011 1101 &
0000 0000 0000 0000 0000 0000 0000 1111
0000 0000 0000 0000 0000 0000 0000 1101 等於13
從上面的過程可以發現key的哈希值的高16 位並不會影響到最終的結果,只有低16 會影響到結果
如果對key進行該操作:( h = key. hashCode ( ) ) ^ ( h >>> 16 )
首先對key的哈希值無符號右移16 位(高位補0 )得到
0000 0000 0000 0000 0100 0011 0011 1001
再異或key的哈希值
0000 0000 0000 0000 0100 0011 0011 1001 ^
0100 0011 0011 1001 1100 0111 0011 1101
0100 0011 0011 1001 1000 0100 0000 0100 (結果)
這樣做的目的:讓它的低16 位擁有了高16 位的特性,從而避免了大量元素落到哈希表同一位置
打個比方:
0100 0111 0011 1001 1100 0111 0011 1101
和 0100 0011 0011 1001 1100 0111 0011 1101
和 0100 0001 0011 1001 1100 0111 0011 1101
分別按位與上一個默認容量(16 - 1 )
0000 0000 0000 0000 0000 0000 0000 1111
他們的結果與高16 位無關
所以得出結論:散列函數這樣做的目的是爲了更好的均勻分配元素在哈希表的位置
3.tableSizeFor方法
給定一個初始化容量,返回一個大於等於該值的2的次方數,作爲最終哈希表初始化容量
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) ;
}
public static 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 >= ( 1 << 30 ) ? ( 1 << 30 ) : n + 1 ) ;
}
1. 假設在初始化HashMap的時候給定的初始化cap是18 ,18 - 1 = 17
換算成二進制那麼就是 0001 0001
2. 經過如上位運算, 最終n變成 0001 1111 等於31
3. 最後再返回31 + 1 = 32 也就是2 的5 次方
多代入幾次你會發現 tableSizeFor方法其實在把給定二進制的低位全部改成1變成單數,最後加1 變成爲2的 次方數,最終返回
4.HashMap爲什麼要用tableSizeFor方法保證哈希表容量一定是2的冪?
哈希碰撞 :兩個不同key經過散列函數hash計算後得到了相同的結果
當把鍵值對put進哈希表中的時候會調用 (n - 1) & hash 表達式,即哈希表最大下標按位與上1個key經歷散列函數的值,以此來確定該元素在哈希表的下標位置
假設現有兩個key的哈希值分別爲01001 和01101
== == == == == == == 當n不爲2 的次方數時== == == == == == =
假設n爲17 那麼 n- 1 的二進制表示爲 10000
然後分別按位與上key的hash值
10000 &
01001 == = 》00000
10000 &
01101 == = 》00000
== == == == == == == = 當n爲2 的次方數時== == == == == == == =
假設n爲16 那麼 n- 1 的二進制表示爲01111
然後分別按位與上key的hash值
01111 &
01001 == = 》01001
01111 &
01101 == = 》01101
它的想法是讓(n-1)的二進制擁有更多的1來與上key的哈希值帶來更大的變數
可以看出當n(哈希表容量)等於2的次方的時候,能更有效的減少hash碰撞,使得元素更均勻的分佈在哈希表上