1.7的HashMap完整实现了哈希表,哈希表是一种根据键值(Key-Value)访问数据的结构,实现这种结构需要解决两个问题:
一. 哈希函数
理想的哈希函数对于不同的输入应该产生不同的结构,同时散列结果应当具有同一性(输出值尽量均匀)和雪崩效应(微小的输入值变化使得输出值发生巨大大变化)
1.7 hash函数实现:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//异或运算 相同返回0
h ^= k.hashCode();
//1.8改为高位参与运算 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
二. 冲突解决
冲突指的哈希函数计算出的访问地址已存在数据,均匀的哈希函数可以减少冲突,但不能避免冲突,发生冲突后,必须解决;也即必须寻找下一个可用地址
HashMap中使用拉链法来解决冲突, 将所有位置重复的数据使用单项链表存储,也就是数组加链表,HashMap使用嵌套类Entry存储元素,它包含四个属性:key,value,hash值和用于单向链表的next
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
下面是代码解析,建议对照着代码看
初始化
HashMap在使用put方法时才会创建Entry对象,同时他的初始大小大于等于2的幂次方,以7和9为例
大于等于7的2的幂次方为8
大于等于9的2的幂次方为16
put方法判断Entry
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
threshold 是你传入的大小,如果没有传入,默认为16:
inflateTable
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 查找一个大于等于toSize的2的幂次方数
int capacity = roundUpToPowerOf2(toSize);
//扩容阈值计算
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建 capacity 大小的Entry 实例
table = new Entry[capacity];
//rehash 扩容之后需要重新计算hash
initHashSeedAsNeeded(capacity);
}
可以看到 new Entry[capacity] ,创建对象时,并没有使用我们传入的toSize,而是将它传入了roundUpToPowerOf2 这个方法
roundUpToPowerOf2
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
关键点在于
Integer.highestOneBit((number - 1) << 1)
这段代码返回一个小于等于number的二的幂次方,并将他左移一位
highestOneBit
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
看不懂?没关系,先打印一下:
下面我以i = 15为例,推算这个过程
第一行代码 i |= (i >> 1)
i >> 1 = 15右移一位(将值转换为2进制,正数时左边补0,最后一位去除) ,计算过程:
15=0000 1111(这里简单写为8位,实际上应该是32位,因为int是4个字节,每个字节占8位)
右移一位=0000 0111
然后将两个值进行 按位或(有一为一)运算
0000 1111
0000 0111
结果为:
0000 1111
此时i = 0000 1111 也就是 15
第二行代码 i |= (i >> 2)
i >> 2 = 15右移两位,计算过程
15=0000 1111
右移两位(最后两位去除,最左边补零)=0000 0011
按位或运算:
0000 1111
0000 0011
结果为:
0000 1111
也就是15
第三行代码 i |= (i >> 4)
15右移四位(最后四位去除),计算过程
15=0000 1111
右移四位=0000 0000
按位或运算:
0000 1111
0000 0000
结果还是0000 1111
至此发现后面的位移运算已经没有意义了,所以直接跳到最后一行代码
返回结果 return i - (i >>> 1)
i >>> 1 = 15无符号右移(不论正负,直接补零)一位
15=0000 1111
无符号右移= 000 0111
然后再相减,结果为:0000 1000
转换为10进制为8
这里有个技巧叫做8421 从高到低对应不同的权重,举例:
0000 1111 = 1* 8 + 1* 4 + 1* 2 + 1* 1 = 15
0000 0111 = 0* 8 + 1* 4 + 1* 2 + 1* 1 = 7
0000 1000 = 1* 8 +0* 4 + 0* 2 + 0* 1 = 8
最后highestOneBit(15)这个方法返回值为 8
我在开头写过 :HashMap初始大小大于等于2的幂次方,8显然不是大于等于15的2的幂次方
我们返回roundUpToPowerOf2方法,查看这段代码:
它将返回值 8 左移1位
8:0000 1000
左移一位(左边去除一位,右边补一位0):0001 0000
转换为10进制为: 16
到这一步就成功的创建了一个大小为16的HashMap!
Integer.highestOneBit((number - 1) 为什么要减一?
我们知道 HashMap的默认大小为1 << 4 也就是16
highestOneBit 会返回小于等于输入值的二的幂次方,传入16会返回16,
此时将16左移一位时会返回32,这样就会导致创建错误大小的HashMap
Put
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold); //初始化
}
if (key == null)
return putForNullKey(value);//key等于null 时,进入这个方法
int hash = hash(key);//哈希函数 计算哈希值
int i = indexFor(hash, table.length);//计算下标
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;//如果这个key已存在,返回老的value
}
}
modCount++; //增加修改次数
addEntry(hash, key, value, i); //添加数据
return null;
}
putForNullKey
private V putForNullKey(V value) {
//判断第一个key是否等于null
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;//覆盖老的value ,并返回
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
indexFor(hash, table.length)
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) ==
1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这一步叫做定位哈希桶索引
h & (length-1) 等价于h%table.length