JDK1.8种的HashMap底层由1.7的数组+链表实现改为了由数组+链表+红黑树来实现,这里只是解读出红黑树以外的常用方法代码的思路。
直接一边看源码,一边加注释,这样过一遍,要好一些。
一、变量解读
/**
* 初始默认容量——16。
* 这里用了移位操作
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量——2的30次幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的加载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化的节点个数的临界值。
* 当添加元素达到整个Map容量达到64并且当前位置上的节点个数达到了该临界值就会将链表转成红黑树。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转成链表的临界值。
* 若当前位置上的节点个数削减到此临界值,则将改为之上的红黑树的存储结构转换为链表结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树化的hashMap容量的临界值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* HashMap种的每个存储位置上的节点的数据类型
* 在JDK1.7种是Entry,1.8种则变成了Node,本问暂时不做解读
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
/**
* 存储node元素的数组
*/
transient Node<K,V>[] table;
/**
* 存储节点的Set
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* HashMap种的当前的元素的数目
*/
transient int size;
/**
* 修改次数,在并发情况下用于抛出异常
*/
transient int modCount;
/**
* 数组扩容的阈值,数组种所存的node元素个数达到此阈值时,就会扩容
*/
int threshold;
/**
* 加载因此,用于计算扩容阈值的
*/
final float loadFactor;
二、构造方法解读
/**
* 该构造方法是实际用于构造一个HashMap
* 只包含容量参数的构造器就是调用了该方法来构造HashMap
*/
public HashMap(int initialCapacity, float loadFactor) {
// 如果传入的初始容量的值小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果传入的初始容量的值大于最大容量的值,那么初始容量的值就修改为最大容量的值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果传入的加载因子的数值不合理,也抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 将传入的加载因子的值赋给当前对象的加载因子
this.loadFactor = loadFactor;
// 计算当前所创建的HashMap对象的扩容阈值
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 调整扩容阈值的方法,经过多次调整,最终使得返回值为大于的传入值的最小的2的整数次幂
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 带有初始容量设置的构造方法,实际上调用了使用两个参数的的构造器,
* 初始容量使用的是传入的参数,加载因子则使用的是默认的加载因子。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 空参的构造器,只是将默认加载银子的值赋给当前对象的加载银子
* 其他成员变量均使用默认值。
* 此构造器构造的HashMap会在第一次添加元素时创建底层数组
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 含Map参数的构造器,
* 会调用putMapEntries方法,来创建一个HashMap,并向其中添加进传入的Map中的元素
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* 该方法用于根据一个Map来为当前的Map对象创建底层数组并其中添加传入Map的元素
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// s时要导入的Map中所存储元素的个数
int s = m.size();
if (s > 0) {
// 如果当前的Map,其底层存储数据的数组为空
if (table == null) { // pre-size
// 不知道这一步为什么要这么操作
float ft = ((float)s / loadFactor) + 1.0F;
// 如果ft小于最大容量那么就t就取ft得整数部分;否则就取最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果t大于当前对象的扩容阈值,那么就再调整一次扩容阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果当前的Map的底层数组不为空,并且导入的Map的元素个数大于当前Map对象的扩容阈值
// 就需要重新调整一下当前Map对象的容量、扩容阈值。(reseize方法还会重新调整元素的存放位置)
else if (s > threshold)
resize();
// 遍历要导入的数组,并将其中的元素一次放入当前的Map对象当中(put方法下文中详细解读)
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
三、put方法解读
/**
* put方法实际上是调用了putVal方法
*/
public V put(K key, V value) {
// 根据传入参数key,获取hashCode值,作为其中一个参数传入到
return putVal(hash(key), key, value, false, true);
}
/**
* 存放元素实际执行的方法
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 首先将当前Map对象的底层存储数组table赋值给tab数组
// 如果tab数组为空或者长度为0,则初始化tab数组,并将其长度赋值给变量n
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化tab数组,并将其长度赋值给变量n
n = (tab = resize()).length;
// 如果tab不为空
// 假设当前tab数组长度为16,那么i = (16 - 1) & hash,i为当前元素索要存放在tab数组中的位置
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果要存放的位置上的元素为null(该位置为空),则将所要存入的元素放到该位置上
tab[i] = newNode(hash, key, value, null);
else { // 如果当前位置上已经存放有别的元素
Node<K,V> e; K k;
// 如果当前位置上的元素p与要存放的元素的hash值相同,
if (p.hash == hash &&
// 并且两个元素的key也相同,或者key的值相同,说明要存放的元素在当前的Map对象中已经存在
((k = p.key) == key || (key != null && key.equals(k))))
// 则将已经存在元素p赋值给对象e
e = p;
// 如果p与待存放元素的hash值不同
// 再判断p是不是一个树结构
else if (p instanceof TreeNode)
// 如果p是树,则调用红黑树的插入方法,插入待存放数据(具体逻辑,后续再具体解读)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 链表插入元素
for (int binCount = 0; ; ++binCount) {
// 判断p的下一个元素是否为空
if ((e = p.next) == null) {
// 若为空,则p的下一个节点就是当前待存放元素所构造的节点
p.next = newNode(hash, key, value, null);
// 添加完成后,判断当前链表上的节点树是否达到转成红黑树结构的阈值
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 若达到树化阈值,则树化
treeifyBin(tab, hash);
break;
}
// 如果p的下一个节点e与待存放的元素相同,则结束循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 指针往下移,等价于 p = p.next
p = e;
}
}
// 经过前面的步骤,变量e完成了赋值操作,接下来是对变量e的value进行操作,主要用于替换e的旧值
// 首先判断e是否为空
if (e != null) { // existing mapping for key
// 将e本来的value赋值给一个Value类型的变量oldValue中
V oldValue = e.value;
// 因为onlyIfAbsent=false,所以一定会有新值替换旧值的操作,无论两个value是否相同
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 该方法没目前整明白,因为该方法方法体为空
afterNodeAccess(e);
// 然后返回旧的值,因为只是value的替换,所以这里modCount没有增加
return oldValue;
}
}
// 增加新的元素后,该变量便+1
++modCount;
// 记录当前Map所存的元素个数的变量size也+1
// 判断添加一个元素后size是否大于扩容阈值,若大于,则进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Map底层存储结构扩容,调整等操作的方法
*/
final Node<K,V>[] resize() {
// 将当前对象的Node数组赋值给oldTab变量
Node<K,V>[] oldTab = table;
// 扩容前的数组的容量和阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// 扩容后的容量和阈值,初始化为0
int newCap, newThr = 0;
if (oldCap > 0) { // 原始容量大于0
// 如果原始数组已经达到了HashMap的最大容量,则只修改阈值,不扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 先将原始容量扩容到2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果原始容量大于等于HashMap的默认初始化容量,并且扩容后的容量小于最大容量
// 将原始阈值放大2倍赋值给新阈值
newThr = oldThr << 1; // double threshold
}
// 如果原始容量为0,但是原始阈值大于0
else if (oldThr > 0)
// 如果调用的是指定了初始容量的或者制定了自定义初始容量和负载因子的构造方法,则进入这一步
// 那么将原始的阈值作为新的容量值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 进入这里,说明HashMap是由空参构造器构造的,并且还没有存放元素,底层数组为空
// 下面两步就是配置当前为数组为空的HashMap的数组的容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY; // 16
// 阈值 = 默认负载因子 * 默认初始化容量(16 * 0.75 = 12)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值为0,则进行相应的计算,算出的值赋值给新阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新阈值赋值给当前HashMap对象的阈值属性
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个新的容量长度的数组(老容量的2倍)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 当前HashMap的table属性更新为新的数组(目前该数组为空)
table = newTab;
if (oldTab != null) { // 如果老数组不为空,则进入判断体中执行相应的步骤
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 先将老数组中第j个元素赋值给变量e
// 判断e是否为空
if ((e = oldTab[j]) != null) { // 如果e不为空
//先将老数组中j位置清空
oldTab[j] = null;
// 判断e是不是只有一个节点
// 如果条件成立,说明e只有一个节点
if (e.next == null)
// 所以便重新计算e在新数组中的位置,并赋值
newTab[e.hash & (newCap - 1)] = e;
// 判断e是不是一棵红黑树
else if (e instanceof TreeNode)
// 若是,则进行红黑树的拆分,并存储到新数组中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 如果e是一个不止一个节点的链表
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) { // 满足该条件的分到lo链表中
// 如果lo链表的尾部节点为空,说明当前lo链表为空
if (loTail == null)
// 则可将e赋值给lo链表的头
loHead = e;
else
// 否则,将e接到lo链表的尾部
loTail.next = e;
// 链表指针后移
loTail = e;
}
else { // 不满足判断条件的数组分到hi链表中
// 链表添加逻辑同lo链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历完之后,将lo链表存放在新数组中的j位置(可理解为老位置)
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将hi链表存放在新的位置
if (hiTail != null) {
hiTail.next = null;
// 新位置 = 老位置j + 老容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回扩容之后的数组
return newTab;
}
四、get方法解读
/**
* 外部使用的get方法
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 真正的获取元素的方法
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果当前HashMap对象的底层数组table不为空,
// 并且table长度不为0,
// 并且根据hash计算出相应位置上的元素不为空,则进入判断内部;否则直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果第一个节点就是所要寻找的节点,则直接返回第一个节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第一个节点不是所要匹配的节点,那么就往下寻找
if ((e = first.next) != null) {
// 判断first是不是一棵红黑树,是的话,就调用寻找树节点的方法
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 遍历节点匹配要寻找的节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 匹配到则返回该节点,否则就继续循环知道链表终止。若最后都没有匹配到,则返回null
return e;
} while ((e = e.next) != null);
}
}
return null;
}
五、remove方法解读
/**
* 外部调用的删除HashMap元素的方法
* 这是根据key来删除元素
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 这是根据key-value来删除元素的方法
*/
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
/**
* 实际删除元素的方法
*/
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;
// 如果当前HashMap对象的底层数组table不为空,
// 并且table长度不为0,
// 并且根据hash计算出相应位置上的元素不为空,则进入判断内部;否则直接返回null。
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;
// 如果当前节点p与待删除的节点相匹配,则将p赋值给变量node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果当前节点不匹配,则往下匹配,将p的下一个节点赋值给变量e,判断e是否为空
else if ((e = p.next) != null) {
// 判断p是不是红黑树,是的话就调用获取树节点的方法
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 如果p不是红黑树,那就是链表,遍历链表找到待删除的元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 匹配到待删除的元素,就将e赋值给变量node,并终止循环
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 判断是否执行删除操作的条件是否满足,
// 若只是根据key来删除元素,matchValue为false,只要node不为空,则进入进行删除操作
// 若根据key-value来删除元素,matchValue为true,node不仅不能为空,还需要value也要匹配到,才能进入进 行删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 判断是否是红黑树,决定是否调用红黑树删除节点的方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果待删除的元素是链表上的第一个节点,则直接将第二个节点放置上来即可
tab[index] = node.next;
else
// 否则,待删除元素的上一个节点的额下一个节点指针指向待删除元素的下一个节点,即可完成删除操作
p.next = node.next;
// 修改次数+1
++modCount;
// 元素个数减一
--size;
afterNodeRemoval(node);
// 返回被删除的元素
return node;
}
}
return null;
}