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的次方的时候,能更有效的避免大量元素落在同一个哈希表位置,使得元素更均匀的分布在哈希表上