HashMap的详细介绍
—–本文只针对1.7版本的HashMap所讲解.
通过上一章的讲解HashMap的详细介绍(1)我们知道了HashMap的构造过程,随即引出了两个问题,一:HashMap的容量为什么一定要是2的次幂?二:如果put元素数量大于阈值 一定会扩容吗?现在我们就来对这俩个问题进行讲解.看代码:
public V put(K key, V value) {
//这里初始化数组,上节课说过了,不清楚的同学请看上节课的介绍
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//判断我们传入的key是否为null,因为我们key不为null,这里我们先不讲解,后面随即讲解,
if (key == null)
return putForNullKey(value);
//对key生成hash码
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
这里对key进行生成hash码,它是怎么生成的呢?我们看下面的代码:
final int hash(Object k) {
int h = hashSeed;//这里为0
//0 !=0 条件不成立,执行后面的.
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//这里通过k的hashCode去和h异或去二次生成hash值返回
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
hashSeed为什么为0可以参考HashMap的hashSeed的问题,然后我们执行indexFor这个方法,进入这个方法我们就能得到答案了,看代码:
//这里传入了两个参数,一个是我们上面生成的hash码,另一个是我们的容器容量.
static int indexFor(int h, int length) {
return h & (length-1);
}
为什么说答案在这里,我们看这里的代码只有一行,看看它做了什么事情,我们默认的长度为16,假设我们的哈希值为100,100的二进制位1100100,16-1为15,15为1111,我们最后得到 1100100 & 1111
最后结果为100,我们在模拟一下,假设传入的是102,得到1100110 & 1111 最后结果为:110. 但是我们的length不是2的次幂会发生什么问题? 假设我们length为6 ,6-1 为5 5的二进制为101 ,还是上面两个数字 先看100 , 1100100 & 101 最后得到结果为 100 , 再看102 1100110 & 101 最后结果也是100 导致他们生成的数值一致,最后所生成的数组位置也形成了一致,最后形成了一个链表结构,链表结构会导致我们查询的效率变低,所以为什么一定要是2的次幂,相信在这里大家都已经知道了,一个就是更好的生成存储位置,更好的解决位置冲突,从而提高查询效率,一个数组的位置,尽量放置一个Entry(也就是put进去的元素).这样查询更快,而不用遍历链表。我们继续往后面看:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//这里执行了indexFor返回了一个索引赋值给i变量
int i = indexFor(hash, table.length);
//table[i] 就是HashMap的数组,也就是拿到数组[i]的位置赋值给e,e如果不为null,则进入,如果没有,则跳出
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//因为我们进去了 肯定e不为null
//所以判断,这次put的元素hash是否等于当前位置的hash值是否一致,如果一致在比较key是否相等,如果相等,那么条件满足,进行覆盖操作,因为我们知道HashMap的key是唯一的,如果key相等,那么执行覆盖
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//把以前的value赋值给oldValue
V oldValue = e.value;
//把这次put的value赋值给e,形成覆盖
e.value = value;
//这里是一个空方法,不需要关注
e.recordAccess(this);
//我们把以前的value返回,所以当我们去覆盖一个元素的时候,你可以拿一个变量去接收,它可以返回旧值.
return oldValue;
}
}
//这个是记录我们对HashMap操作的次数,但是我们可以看见,
//在1.7的时候,如果我们只是修改数据,它不会执行到这里,这里也就不能自增
modCount++;
//这里就是添加新的元素了,看看下面代码,我们就可以解决第二个问题了
addEntry(hash, key, value, i);
return null;
}
上面第一个问题已经解决,第二个问题,如果put元素数量大于阈值 一定会扩容吗?
答案是不一定(1.8是会扩容的!),看代码:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,怎么扩容的 ,同学们好好思考一下?
resize(2 * table.length);
//扩容后,会再次重新计算我们的hash值
hash = (null != key) ? hash(key) : 0;
//在重新计算我们的位置
bucketIndex = indexFor(hash, table.length);
}
//这里是真正添加元素的地方,这里传入了4个参数,它们都和这次put进去的元素,是息息相关的
//参数1:它的hash值,参数2:它的key,参数3:它的value,参数4:它生成的位置
createEntry(hash, key, value, bucketIndex);
}
当我们添加元素的时候,它会比较size是否大于阀值,如果大于它还会去比较你生成位置是否为null,看这里如果我们的生成的位置如果不为null 就算大于阈值也不会去扩容的。所以我们这里第二个问题也随即解决。然后我们再来看看它是怎么添加元素的,看代码:
//参数1:它的hash值,参数2:它的key,参数3:它的value,参数4:它生成的位置
void createEntry(int hash, K key, V value, int bucketIndex) {
//这里把当前位置元素(null)赋值给e
Entry<K,V> e = table[bucketIndex];
//看我们这里new了一个Entry对象,这里就创建了一个新的对象
//给当前生成的位置table[bucketIndex],这里就添加成功
table[bucketIndex] = new Entry<>(hash, key, value, e);
//纪录元素的个数
size++;
}
同学们想一想如果e不为null会是什么情况?这里应该能看出来的,同学们好好想一想,上面的代码应该能看出来。还有我上面跳过的问题,当key为null时,它又会怎么操作?好了,这一章我们就到这里,下期再见.
书山有路勤为径,学海无涯苦作舟。