下图是JDK 1.8中HashMap在Idea中查看的整体结构,关于最重要的部分,我在图中添加了文字说明。下面将从默认静态常量,成员变量,构造函数,核心方法,迭代遍历,与JDK1.7中的不同等6个方面进行学习。
对HashMap源码最详细的注释我都写到这里面了,可以点击链接下载
1 六个静态常量
能搜到这篇文章,说明你对散列,链表,HashMap等知识有一定了解了。
// 默认初始容量 - 必须是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默认最大容量 - 如果具有参数的任一构造函数隐式指定更高的值,则使用此方法。必须是2的幂 <= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值:一个链表大于这个值就要转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 反树化阈值:一个红黑树小于这个值就要转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小树形化容量。当哈希表中的容量大于这个值时,表中的桶才能进行树形化
* 否则桶内元素太多时会扩容,而不是树形化,为了避免进行扩容、树形化选择
* 的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
*/
static final int MIN_TREEIFY_CAPACITY = 64;
这里可能比较绕的是三个树化相关的值理解,什么时候开始树化呢?答:哈希表容量必须要比MIN_TREEIFY_CAPACITY大,否则即使某个链表值大于TREEIFY_THRESHOLD也不会树化,而是进行扩容。
为什么capacity都要要求是二的整数幂?答:为了计算简便和提高效率啊,因为2的幂次用二进制表示非常方便高效的计算,后面计算数组索引位置取模运算使用的hash&(n-1)就是例子,看了后面的代码会更好理解。
2 六个成员变量
直接先看代码注释就理解的差不多了
// HashMap的桶位,是一个Node节点数组
transient Node<K,V>[] table;
// 缓存保存根据HashMap获取的Set集合
transient Set<Map.Entry<K,V>> entrySet;
// 当前存储的键值对个数
transient int size;
/*
修改次数,因为HashMap是线程不安全,如果在迭代的过程中HashMap被其他线程修改了,
modCount的数值就会发生变化, 这个时候expectedModCount和ModCount不相等, 迭
代器就会抛出ConcurrentModificationException()异
*/
transient int modCount;
/*
自动扩容阈值(threshold = capacity*loadFactor), capacity是当前桶位数,
loadFactor是加载因子,默认0.75f;也就是当HashMap数据条数达到桶位数的75%时,
就会自动扩容
*/
int threshold;
//加载因子(默认0.75f), size/capacity >= 0.75 = threshold/capacity 时将会扩容
final float loadFactor;
实际上不止六个成员变量,因为我们还从AbstractMap中继承了keySet, values,所以总共加起来有8个左右。
table 是个数组,该数组的每一个位置存放的可能都是一个链表或者一颗红黑树的首(根)节点,也就是HashMap真正存键值对的容器。
modeCount 主要是防止在遍历等操作是修改HashMap的结构,在具有修改HashMap结构的方法(removeNode,putVal等)中该计数器都会在修改成功后增加。
threshold,capacity,loadFactor,size他们之间的关系在代码注释中已经说得很详细了。
3 构造函数
public HashMap(int initialCapacity, float loadFactor)...
public HashMap(int initialCapacity)...
public HashMap()...
public HashMap(Map<? extends K, ? extends V> m)...
总共有上面4个构造方法,详细源码就略了。
前3个构造函数 一看就知道啥意思了,无非自己指定容量值和加载因子。这里有一点需要注意,在2个参数的构造方法中,初始化的是threshold和loadFactor,所以我估计这里实际扩容是在添加时候判断阈值和capacity时才会真正扩容。
第4个构造函数 该构造函数调用putMapEntries往里面添加另外一个Map的所有数据。
4 核心方法
什么是核心方法,当然实现HashMap最基础操作的增加,查找,扩容,删除这几个方法。在上面的图中用加粗红色字体注释了,其他的一些方法是调用这几个主要方法实现自身功能的,图中用 @符号+黄色字体 表示。看懂这几个核心方法,@符号标注的那些方法就非常容易理解。
4.1 增加
看源码发现外部调用实际是调用的put,而实际代码逻辑在putVal方法中。
public V put(K key, V value) {
// 调用putVal方法
return putVal(hash(key), key, value, false, true);
}
/**
* @Description: HashMap插入一个值的方法
* @param hash key的散列值
* @param key 键对象
* @param value 要保存的值对象
* @param onlyIfAbsent 如果为true,不要替换存在的值对象
* @param evict 如果为false,则表处于创建模式
* @return 返回旧值,如果不存在返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; // HashMap的桶位数组,存放所有的键值对
Node<K,V> p; // 表示定位到的桶位tab[i]
int n, i; // n表示tab数组大小,i表示定位到的桶位索引
// 1. 判断是否要resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 定位到要放的桶位,并进行修改
// 2.1 tab[i] == null: 还没放过任何值,就直接放在该处
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 2.2 tab[i] != null: 遍历该处的链表或树,找到要放的位置
else {
Node<K,V> e; K k;
// 2.2.1 和tab[i]的key相匹配了
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 2.2.2 首节点tab[i]是一个TreeNode
else if (p instanceof TreeNode) // 是树的根节点,调用putTreeVal插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 2.2.3 首节点是链表的节点
else {
for (int binCount = 0; ; ++binCount) {
// 尾插法:找到最末端,插入一个节点,放入key和value
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果插入后,链表长度值大于树化的阈值,就将链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 该key映射已经存在,直接跳出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
// p 指向下一个非空节点
p = e;
}
}
// 2.2.4 这里才是真正放值的时候,如果key重复, 根据onlyIfAbsent判断是否需要覆盖旧值
if (e != null) {
V oldValue = e.value; // 该节点旧值
// 根据onlyIfAbsent判断是否要覆盖旧的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 回调方法
return oldValue; // 返回旧值,如果返回旧值就不会执行下面逻辑,modCount也不会加1
}
}
// 3 调整
// 修改次数加1
++modCount;
// size加一后,看要扩容不
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 回调方法
return null;
}
该方法重要的是插入方法:使用了尾插法。而JDK1.7使用的是头插法。这点将在JDK1.7和1.8比较中讲解。
4.2 查找
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; // tab临时存放table
/*first是key所在桶位的第一个节点,first = tab[(n - 1) & hash]
* */
Node<K,V> first, e;
int n; // n表示table键值对数量
K k; // 存放节点key的临时变量
/*只有在table非空,且根据hash值得到对应桶位非空时,才开始查找对应桶位上的链表或红黑树*/
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 该桶位第一个节点满足“hash相同&&(key地址相等,或者key内容相同)”,就直接返回first
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 接着搜索first下面的节点
if ((e = first.next) != null) {
if (first instanceof TreeNode) // 如果是红黑树,就调用getTreeNode搜索该树
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { // 依次判断链表的每一个节点,直到找到return或者遍历完该链表
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
4.3 扩容
/*扩容方法*/
final Node<K,V>[] resize() {
// 1. 初始化必要的临时变量
Node<K,V>[] oldTab = table; // 暂存旧容器
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量
int oldThr = threshold; // 旧阈值
int newCap, newThr = 0; // 初始化新的容量和阈值
// 2. 计算新阈值和新容量
if (oldCap > 0) { // 如果旧容量大于0
if (oldCap >= MAXIMUM_CAPACITY) { // 旧容量大于最大允许容量
threshold = Integer.MAX_VALUE; // 阈值设为最大
return oldTab; // 无法再扩容,直接返回旧容器
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)// 否则新容量加倍
newThr = oldThr << 1; // 新容量没超过最大容量,阈值加倍
}else if (oldThr > 0)
newCap = oldThr; // 用旧阈值代替新容量
else { // 否则用默认值初始化新容量和新阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 上面分支计算完,如果新阈值为0
float ft = (float)newCap * loadFactor; // 计算阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 得到最终阈值更新
@SuppressWarnings({"rawtypes","unchecked"})
// 3. 再散列过程
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 根据新容量建一个新容器
table = newTab; // 用新容器替代原来的容器
if (oldTab != null) { // 如果旧容器非空,将旧容器中的值分配到新容器中
for (int j = 0; j < oldCap; ++j) { // 遍历旧容器的每一个桶位
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 该桶位赋值给e,如果非空证明有元素需要分配
oldTab[j] = null; // 置空旧桶的该位置
if (e.next == null) //e.next为空,该桶位只有一个节点
// 直接存给新桶,这里通过e.hash & (newCap - 1)定位到新桶的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 如果是树结构的根节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // ???
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历桶中的链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 索引值不变
if (loTail == null)
loHead = e; // 头结点
else
loTail.next = e; // 连接新元素
loTail = e; // loTail指向尾结点
}
else { // 索引值改变
if (hiTail == null)
hiHead = e; // 头结点
else
hiTail.next = e; // 连接新元素
hiTail = e; // hiTail指向尾结点
}
} while ((e = next) != null);
// 保存到(j)位置,即原位置
if (loTail != null) {
loTail.next = null; // 尾结点下一个节点设为null
newTab[j] = loHead; // 新容器数组前半段
}
// 保存到(oldCap + j)位置,即后半段
if (hiTail != null) {
hiTail.next = null; // 尾结点下一个节点设为null
newTab[j + oldCap] = hiHead; // 新容器数组后半段
}
}
}
}
}
return newTab;
}
扩容的时候要注意是怎么计算
4.4 删除
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 1. 查找要删除的节点位置
// 1.1 要删除的直接是桶位首节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 1.2 在红黑树中搜索
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 1.3 在链表中搜索
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e; // 找到要删除的节点,赋给node
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 2.2 删除节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是树,调用removeTreeNode删除该节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果是桶位第一个节点,将下一个节点直接放到该桶位
else if (node == p)
tab[index] = node.next;
// 如果是链表中间某个节点node, 则前一个节点p直接指向该节点下一个节点
else
p.next = node.next;
++modCount; // 调整HashMap修改次数
--size; // 调整键值对计数器
afterNodeRemoval(node); // 回调
return node; // 删除成功,返回被删除的节点
}
}
return null; // 没有删除返回null
}
5 遍历
5.1 entrySet,keySet,values遍历的底层实现
HashMap底层是如何实现遍历的呢? 可以看看我的这篇文章详解Java中散列和HashMap中的第4.3.4节,分析HashMap是怎样实现遍历的,其实keySet,entrySet,values的遍历底层原理是非常相似的。
集合类的常用遍历方法总结 可以看看我的这篇文章Java集合类详解中的第8节
5.2 JDK新增的Spliterator遍历
6 JDK8与JDK7的对比
推荐这篇文章,等我整理的更好在详细总结这部分。
美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析