概述
从本文你可以学习到:
- 什么时候会使用 HashMap ?他有什么特点?
- 你知道 HashMap 的工作原理吗?
- 你知道 get 和 put 的原理吗?
equals()
和hashCode()
的都有什么作用? - 你知道 hash 的实现吗?为什么要这样实现?
- 如果 HashMap 的大小超过了负载因子(load factor)定义的容量,怎么办?
- 为什么 HashMap 的容量是2的 n 次幂的形式?
在说明这些问题的同时, 我从JDK7——JDK8的 HashMap 的变化来说明开发人员对这个数据结构的优化,重点放在了 put() 函数
和 resize() 函数
,还结合了《码出高效》这本书指出了 HashMap 在并发情况下表现出来的问题。
注意:源码可能与JDK中实际代码略有不同, 这里面JDK7版以《码出高效》为准,JDK8版本以网络版本为准,意在说明某个函数功能, 便于理解。在优快云上,不能上传bmp格式图片,大家感兴趣的话,访问我的个人博客,那里排版更清晰,图片展示更明了
两个重要参数说起
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
Capacity 就是 bucket 的大小,Load factor就是 bucket 填满程度的最大比例。如果对迭代性能要求很高的话,不要把 capacity 设置过大,也不要把 load factor 设置过小。当bucket中的entries的数目大于capacity*load factor 时,就需要调整 bucket 的大小为当前的2倍。
put函数的实现
put函数大致的思路为:
- 对key的
hashCode()
做 hash ,然后再计算 index ; - 如果没碰撞直接放到 bucket 里;
- 如果碰撞了,以链表的形式存在 buckets 后;
- 如果碰撞导致链表过长(大于等于
TREEIFY_THRESHOLD
),就把链表转换成红黑树; - 如果节点已经存在就替换 old value(保证key的唯一性)
- 如果 bucket 满了(超过
load factor * current capacity
),就要resize。
具体代码的实现如下:
JDK7 的 put
public V put(K key, V value) {
int hash = hash(key);
int i = indexFor(hash, table.length);
//此循环通过 hashCode 返回值找到对应的数组下标位置
//如果 equals 结果为真,则覆盖原值, 如果都为 false ,则添加元素
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果key的 hash 是相同的,那么在进行如下判断
//key 是同一个对象或者 equals 返回为真, 则覆盖原来的Value值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果元素的个数达到 threshold 的扩容阈值且数组下标位置已经存在元素,则进行扩容
if ((size++ >= threshold) && (null != table[bucketIndex])){
//扩容 2 倍, size 是实际存放元素的个数,而 length 是数组的容量大小(capacity)
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//插入元素时, 应该插入在头部, 而不是尾部
void createEntry(int hash. K key, V value, int bucketIndex){
//不管原来的数组对应的下标是否为 null ,都作为 Entry 的 BucketIndex 的 next值
Entry<K,V> e = table[bucketIndex]; (***)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
关于并发的问题:
如上源码, 在 createEntry()
方法中,新添加的元素直接放在 slot 槽( slot 哈希槽,table[i] 这个位置)使新添加的元素在下一次提取后可以更快的被访问到。 如果两个线程同时执行 (***) 处时, 那么一个线程的赋值就会被另一个覆盖掉, 这是对象丢失的原因之一。 我们构造一个 HashMap 集合,把所有元素放置在同一个哈希桶内, 达到扩容条件后,观察一下 resize()
方法是如何进行数据迁移的。示例代码和图可参考《码出高效》P204
JDK8 的 put
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false