开篇
本篇博客主要分析 jdk1.7 中的 HashMap 的 put() 方法。接下来是几点说明:
- 通过画流程图的方式分析方法的执行流程,不会细致到具体每个方法,比如 hash 算法。
- 不讲 HashMap 的相关概念以及使用方法, 可能只会提一下。
- 文章贴出的代码注释很重要。
- 具体细节之后会慢慢补充, 循序渐进。
进入正题
预备知识
jdk1.7 中 HashMap 采用的是数组 + 链表的数据结构(ps:如果数组和链表是什么东西,不建议往下看),如下图所示:
当 put 一个元素时, 会先计算出元素在数组中的所在位置, 如果那个位置已经有元素存在, 这时就发生了哈希冲突(或叫哈希碰撞)。至于发生哈希冲突后, 稍后详解。
注意: 这张图很重要, 下面会反复提到
HashMap 源码中的数组和链表如何定义的?
链表中的节点定义为 Entry
// 链表中的节点
static class Entry implements Map.Entry {
final K key; // 存放 键
V value; // 存放 值
Entry next;// 指向链表中的下一个节点
int hash;// 节点对应的hash值
// 此处省略 set/get 等方法
}
// 数组 : 默认为空数组
transient Entry[] table = (Entry[]) EMPTY_TABLE;
这里插一句:
比如上面的最后一行代码,里面有一个 transient 关键字, 它的的用法和含义我们暂时不用了解,着急的话请自行学习,后续的文章可能也会补充,本文我们只讲流程,它并不会妨碍我们。 说这句话是因为我之前看的时候,就想把每一处都了解清楚,从而导致耗费了很多时间,最后发现脑子里一团浆糊, 这里再次强调 循序渐进。
由数组和链表的定义,我们能得出什么?
数组中可以存放一个 Entry,也可以存放一个 Entry 链。(结合上面的图)
HashMap 中的哈希算法?
把 put 方法传入的 key,经过算法计算,得到一个 int 类型的数字并返回。
key 为 null 的元素放在哪?
数组(table[])中索引为 0 的位置, (再次结合上面的图)
比较重要的几个变量
这里说几个 put 方法中用到的几个变量。
// 一个大小为0 的空数组
static final Entry,?>[] EMPTY_TABLE = {};
// 存放元素的数组,默认指向上面的空数组
transient Entry[] table = (Entry[]) EMPTY_TABLE;
put 方法的源代码(无注释)
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry 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;
}
put 方法的源代码(有注释)
public V put(K key, V value) {
// 如果容器为空, 就初始化容器
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 为null,放置在第 0 个 桶的位置
if (key == null)
return putForNullKey(value);
// 根据 key 计算 hash值
int hash = hash(key);
// 根据 hash 值计算出应该存放到数组中的哪个位置(暂时命名为位置 A)
int i = indexFor(hash, table.length);
// 判断位置 A 是否已经被占据,如果是,就遍历该位置上的链表
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 如果链表中满足以下条件(命名为条件1)的 Entry ,就覆盖掉它
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;// 略过,不做分析
// 如果位置 A上是空的
// 或者位置 A 中的链表不存在满足条件1的 Entry,就创建一个Entry 放置在位置A, 充当链表表头。
addEntry(hash, key, value, i);
return null;
}
流程图
这里我们就看下完整的流程图是什么样的。
总结
上面很多细节都没讲, 比如:hash 算法是怎么实现的,为什么? HashMap 的扩容机制,扩容因子为什么是 0.75,为什么是两倍扩容,甚至其他更难的点,HashMap 为什么是非线程安全的等等。 上面这些问题是我学习 HashMap 源码时遇到的问题,基本看到一个方法就想点进去看细节,最后发现没头没尾的。而本文只是先描述下 put 方法的大致执行流程,之后会逐个击破上面提到的问题, 循序渐进。