jdk1.8之后对HashMap做了很大的优化,把原有的(数组+链表)结构改成了(数组+链表+红黑树)。
当链表的长度大于8的时候,自动转换成红黑树。把原来的Entry改为节点Node。jdk1.8中HashMap的数据结构如下:
基本属性
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子0.75
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8; // 链表节点转换红黑树节点的阈值, 9个节点转
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点转换链表节点的阈值, 6个节点转
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64; // 转红黑树时, table的最小长度
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> { // 基本hash节点, 继承自Entry
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;
}
}
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {// 红黑树节点
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// ...
}
定位节点下标
计算hash值的运算比1.7的更简单。计算节点下标和1.7的算法是一样的,这里就不详细描述,具体请看我的上一篇文章:https://www.jianshu.com/p/2d8647961ab5
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
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;
// table不为空 && table长度大于0 && table索引位置(根据hash值计算出)不为空
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; // first的key等于传入的key则返回first对象
if ((e = first.next) != null) { // 向下遍历
if (first instanceof TreeNode) // 判断是否为TreeNode
// 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 走到这代表节点为链表节点
do { // 向下遍历链表, 直至找到节点的key和传入的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 找不到符合的返回空
}
1,先判断table不为空,且table的长度大于0,且table索引位置不为空
2,检查第一个节点,如果第一个节点就是要的值,直接返回。
3,向下遍历链表,判断是否为treeNode,如果是就调用getTreeNode
4,如果不是红黑树,就遍历链表找到数据
5,如果都找不到直接返回null
接下来看getTreeNode
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 使用根结点调用find方法
// 查找根节点, 索引位置的头节点并不一定为红黑树的根结点
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 从调用此方法的结点开始查找, 通过hash值和key找到对应的节点
* 此处是红黑树的遍历, 红黑树是特殊的自平衡二叉查找树
* 平衡二叉查找树的特点:左节点<根节点<右节点
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this; // this为调用此方法的节点
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h) // 传入的hash值小于p节点的hash值, 则往p节点的左边遍历
p = pl; // p赋值为p节点的左节点
else if (ph < h) // 传入的hash值大于p节点的hash值, 则往p节点的右边遍历
p = pr; // p赋值为p节点的右节点
// 传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null) // p节点的左节点为空则将向右遍历
p = pr;
else if (pr == null) // p节点的右节点为空则向左遍历
p = pl;
else if ((kc != null ||
// 如果传入的key(k)所属的类实现了Comparable接口,则将传入的key跟p节点的key比较
(kc = comparableClassFor(k)) != null) && // 此行不为空代表k实现了Comparable
(dir = compareComparables(kc, k, pk)) != 0)//k<pk则dir<0, k>pk则dir>0
p = (dir < 0) ? pl : pr; // k < pk则向左遍历(p赋值为p的左节点), 否则向右遍历
// 代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
else// 代码走到此处代表上一个向右遍历(pr.find(h, k, kc))为空, 因此直接向左遍历
p = pl;
} while (p != null);
return null;
}
1,将p节点赋值为调用此方法的节点
2,如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
3,如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
4,如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
5,如果p的左节点为空则向右遍历,反之如果p的右节点为空则向左遍历
6,如果传入的key(即代码中的参数变量k)所属的类实现了Comparable接口(kc不为空,comparableClassFor方法见下文代码块3),则将传入的key跟p节点的key进行比较(kc实现了Comparable接口,因此通过kc的比较方法进行比较),并将比较结果赋值给dir,如果dir<0则代表k<pk,则向p节点的左边遍历(pl);否则,向p节点的右边遍历(pr)。
7,代码走到此处,代表key所属类没有实现Comparable,因此直接指定向p的右边遍历,如果能找到目标节点则返回
8,代码走到此处代表与第7点向右遍历没有找到目标节点,因此直接向左边遍历
9,以上都找不到目标节点则返回空
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
如果x实现了Comparable接口,则返回 x的Class。
put方法
public V put(K key, V value) {
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;
// table是否为空或者length等于0, 如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过hash值计算索引位置, 如果table表该索引位置节点为空则新增一个
if ((p = tab[i = (n - 1) & hash]) == null)// 将索引位置的头节点赋值给p
tab[i] = newNode(hash, key, value, null);
else { // table表该索引位置不为空
Node<K,V> e; K k;
if (p.hash == hash && // 判断p节点的hash值和key值是否跟传入的hash值和key值相等
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果相等, 则p节点即为要查找的目标节点,赋值给e
// 判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 走到这代表p节点为普通链表节点
for (int binCount = 0; ; ++binCount) { // 遍历此链表, binCount用于统计节点数
if ((e = p.next) == null) { // p.next为空代表不存在目标节点则新增一个节点插入链表尾部
p.next = newNode(hash, key, value, null);
// 计算节点是否超过8个, 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);// 如果超过8个,调用treeifyBin方法将该链表转换为红黑树
break;
}
if (e.hash == hash && // e节点的hash值和key值都与传入的相等, 则e即为目标节点,跳出循环
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// e不为空则代表根据传入的hash值和key值查找到了节点,将该节点的value覆盖,返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
if (++size > threshold) // 插入节点后超过阈值则进行扩容
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
1,校验table是否为空或者length等于0,如果是则调用resize方法(见下文resize方法)进行初始化
2,通过hash值计算索引位置,将该索引位置的头节点赋值给p节点,如果该索引位置节点为空则使用传入的参数新增一个节点并放在该索引位置
3,判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
4,如果p节点不是目标节点,则判断p节点是否为TreeNode,如果是则调用红黑树的putTreeVal方法(见下文代码块4)查找目标节点
5,走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,并定义变量binCount来统计该链表的节点数
6,如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部,并校验节点数是否超过8个,如果超过则调用treeifyBin方法(见下文代码块6)将链表节点转为红黑树节点
7,如果遍历的e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
8,如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
9,如果插入节点后节点数超过阈值,则调用resize方法(见下文resize方法)进行扩容
继续看putTreeVal方法
/**
* Tree version of putVal.
* 红黑树插入会同时维护原来的链表属性, 即原来的next属性
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 查找根节点, 索引位置的头节点并不一定为红黑树的根结点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) { // 将根节点赋值给p, 开始遍历
int dir, ph; K pk;
if ((ph = p.hash) > h) // 如果传入的hash值小于p节点的hash值
dir = -1; // 则将dir赋值为-1, 代表向p的左边查找树
else if (ph < h) // 如果传入的hash值大于p节点的hash值,
dir = 1; // 则将dir赋值为1, 代表向p的右边查找树
// 如果传入的hash值和key值等于p节点的hash值和key值, 则p节点即为目标节点, 返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) { // 第一次符合条件, 该方法只有第一次才执行
TreeNode<K,V> q, ch;
searched = true;
// 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
dir = tieBreakOrder(k, pk); // dir<0则代表k<pk,则向p左边查找;反之亦然
}
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
// dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 走进来代表已经找到x的位置,只需将x放到该位置即可
Node<K,V> xpn = xp.next; // xp的next节点
// 创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0) // 如果时dir <= 0, 则代表x节点为xp的左节点
xp.left = x;
else // 如果时dir> 0, 则代表x节点为xp的右节点
xp.right = x;
xp.next = x; // 将xp的next节点设置为x
x.parent = x.prev = xp; // 将x的parent和prev节点设置为xp
// 如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x)); // 进行红黑树的插入平衡调整
return null;
}
}
}
1,查找当前红黑树的根结点,将根结点赋值给p节点,开始进行查找
2,如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树
3,如果传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右边查找树
4,如果传入的hash值等于p节点的hash值,并且传入的key值跟p节点的key值相等, 则该p节点即为目标节点,返回p节点
5,如果k所属的类没有实现Comparable接口,或者k和p节点的key使用compareTo方法比较相等:第一次会从p节点的左节点和右节点分别调用find方法(见上文代码块2)进行查找,如果查找到目标节点则返回;如果不是第一次或者调用find方法没有找到目标节点,则调用tieBreakOrder方法(见下文代码块5)比较k和p节点的key值的大小,以决定向树的左节点还是右节点查找。
6,如果dir <= 0则向左节点查找(p赋值为p.left,并进行下一次循环),否则向右节点查找,如果已经无法继续查找(p赋值后为null),则代表该位置即为x的目标位置,另外变量xp用来记录查找的最后一个节点,即下文新增的x节点的父节点。
7,以传入的hash、key、value参数和xp节点的next节点为参数,构建x节点(注意:xp节点在此处可能是叶子节点、没有左节点的节点、没有右节点的节点三种情况,即使它是叶子节点,它也可能有next节点,红黑树的结构跟链表的结构是互不影响的,不会因为某个节点是叶子节点就说它没有next节点,红黑树在进行操作时会同时维护红黑树结构和链表结构,next属性就是用来维护链表结构的),根据dir的值决定x决定放在xp节点的左节点还是右节点,将xp的next节点设为x,将x的parent和prev节点设为xp,如果原xp的next节点(xpn)不为空, 则将该节点的prev节点设置为x节点, 与上面的将x节点的next节点设置为xpn对应。
8,进行红黑树的插入平衡调整,见文末的解释2。
接着查看treeifyBin,将链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// table为空或者table的长度小于64, 进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 根据hash值计算索引值, 遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null); // 链表节点转红黑树节点
if (tl == null) // tl为空代表为第一次循环
hd = p; // 头结点
else {
p.prev = tl; // 当前节点的prev属性设为上一个节点
tl.next = p; // 上一个节点的next属性设置为当前节点
}
tl = p; // tl赋值为p, 在下一次循环中作为上一个节点
} while ((e = e.next) != null); // e指向下一个节点
// 将table该索引位置赋值为新转的TreeNode的头节点
if ((tab[index] = hd) != null)
hd.treeify(tab); // 以头结点为根结点, 构建红黑树
}
}
1,校验table是否为空,如果长度小于64,则调用resize方法(见下文resize方法)进行扩容。
2,根据hash值计算索引值,将该索引位置的节点赋值给e节点,从e节点开始遍历该索引位置的链表。
3,调用replacementTreeNode方法(该方法就一行代码,直接返回一个新建的TreeNode)将链表节点转为红黑树节点,将头结点赋值给hd节点,每次遍历结束将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)。
4,将table该索引位置赋值为新转的TreeNode的头节点hd,如果该节点不为空,则以hd为根结点,调用treeify方法构建红黑树。
final void treeify(Node<K,V>[] tab) { // 构建红黑树
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即为调用此方法的TreeNode
next = (TreeNode<K,V>)x.next; // next赋值为x的下个节点
x.left = x.right = null; // 将x的左右节点设置为空
if (root == null) { // 如果还没有根结点, 则将x设置为根结点
x.parent = null; // 根结点没有父节点
x.red = false; // 根结点必须为黑色
root = x; // 将x设置为根结点
}
else {
K k = x.key; // k赋值为x的key
int h = x.hash; // h赋值为x的hash值
Class<?> kc = null;
// 如果当前节点x不是根结点, 则从根节点开始查找属于该节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) // 如果x节点的hash值小于p节点的hash值
dir = -1; // 则将dir赋值为-1, 代表向p的左边查找
else if (ph < h) // 与上面相反, 如果x节点的hash值大于p节点的hash值
dir = 1; // 则将dir赋值为1, 代表向p的右边查找
// 走到这代表x的hash值和p的hash值相等,则比较key值
else if ((kc == null && // 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
// dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp; // x的父节点即为最后一次遍历的p节点
if (dir <= 0) // 如果时dir <= 0, 则代表x节点为父节点的左节点
xp.left = x;
else // 如果时dir > 0, 则代表x节点为父节点的右节点
xp.right = x;
// 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root); // 如果root节点不在table索引位置的头结点, 则将其调整为头结点
}
/**
* 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
* 将root放到头节点的位置, 原头节点放在root的next节点上
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) { // 如果root节点不是该索引位置的头节点
Node<K,V> rn;
tab[index] = root; // 将该索引位置的头节点赋值为root节点
TreeNode<K,V> rp = root.prev; // root节点的上一个节点
// 如果root节点的下一个节点不为空,
// 则将root节点的下一个节点的prev属性设置为root节点的上一个节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 如果root节点的上一个节点不为空,
// 则将root节点的上一个节点的next属性设置为root节点的下一个节点
if (rp != null)
rp.next = rn;
if (first != null) // 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
first.prev = root;
root.next = first; // 将root节点的next属性设置为原头节点
root.prev = null;
}
assert checkInvariants(root); // 检查树是否正常
}
}
resize
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 老table不为空
if (oldCap >= MAXIMUM_CAPACITY) { // 老table的容量超过最大容量值
threshold = Integer.MAX_VALUE; // 设置阈值为Integer.MAX_VALUE
return oldTab;
}
// 将新容量赋值为老容量*2,如果新容量<最大容量并且老容量>=16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值
newCap = oldThr; // 则将新表的容量设置为老表的阈值
else { // 老表的容量为0, 老表的阈值为0, 则为空表,设置默认容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 如果新阈值为空, 则通过新的容量*负载因子获得新阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 将当前阈值赋值为刚计算出来的新的阈值
@SuppressWarnings({"rawtypes","unchecked"})
// 定义新表,容量为刚计算出来的新容量
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) { // 将索引值为j的老表头节点赋值给e
oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
// 如果e.next为空, 则代表老表的该位置只有1个节点,
// 通过hash值计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 调用treeNode的hash分布(跟下面最后一个else的内容几乎相同)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
Node<K,V> next;
do {
next = e.next;
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null; // 最后一个节点的next设为空
newTab[j] = loHead; // 将原索引位置的节点设置为对应的头结点
}
if (hiTail != null) {
hiTail.next = null; // 最后一个节点的next设为空
newTab[j + oldCap] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
}
}
}
}
}
return newTab;
}
1,如果老表的容量大于0,判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表(此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大);将新表的容量赋值为老表的容量*2,如果新容量小于最大容量并且老容量不小于16,则直接将新的阈值设置为原来的两倍。
2,如果老表的容量为0,老表的阈值大于0,这种情况是传了容量的new方法创建的空表,将新表的容量设置为老表的阈值(这种情况发生在新创建的HashMap第一次put时,该HashMap初始化的时候传了初始容量,由于HashMap并没有capacity变量来存放容量值,因此传进来的初始容量是存放在threshold变量上(查看HashMap(int initialCapacity, float loadFactor)方法),因此此时老表的threshold的值就是我们要新创建的HashMap的capacity,所以将新表的容量设置为老表的阈值。
3,如果老表的容量为0,老表的阈值为0,这种情况是没有传容量的new方法创建的空表,将阈值和容量设置为默认值。
4,如果新表的阈值为空,则通过新的容量 * 负载因子获得阈值(这种情况是初始化的时候传了初始容量,跟第2点相同情况,或者初始容量设置的太小导致老表的容量没有超过16导致的)。
5,将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将当前的表设置为新定义的表。
6,如果老表不为空,则需遍历所有节点,将节点赋值给新表。
7,将老表上索引为j的头结点赋值给e节点,并将老表上索引为j的节点设置为空。
8,如果e的next节点为空,则代表老表的该位置只有1个节点,通过hash值计算新表的索引位置,直接将该节点放在新表的该位置上。
9,如果e的next节点不为空,并且e为TreeNode,则调用split方法(见下文代码块10)进行hash分布。
10,如果e的next节点不为空,并且e为普通的链表节点,则进行普通的hash分布。
11,如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为0,则说明e节点扩容后的索引位置跟老表的索引位置一样(见例子1),进行链表拼接操作:如果loTail为空,代表该节点为第一个节点,则将loHead赋值为该节点;否则将节点添加在loTail后面,并将loTail赋值为新增的节点。
12,如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为1,则说明e节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1),进行链表拼接操作:如果hiTail为空,代表该节点为第一个节点,则将hiHead赋值为该节点;否则将节点添加在hiTail后面,并将hiTail赋值为新增的节点。
13,老表节点重新hash分布在新表结束后,如果loTail不为空(说明老表的数据有分布到新表上原索引位置的节点),则将最后一个节点的next设为空,并将新表上原索引位置的节点设置为对应的头结点;如果hiTail不为空(说明老表的数据有分布到新表上原索引+oldCap位置的节点),则将最后一个节点的next设为空,并将新表上索引位置为原索引+oldCap的节点设置为对应的头结点。
14,返回新表。
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到调用此方法的节点
TreeNode<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点
TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历
next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点
e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收
//如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
++lc; // 统计原索引位置的节点个数
}
//如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
++hc; // 统计索引位置为原索引+oldCap的节点个数
}
}
if (loHead != null) { // 原索引位置的节点不为空
if (lc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead; // 将原索引位置的节点设置为对应的头结点
// hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (hiHead != null)
loHead.treeify(tab); // 以loHead为根结点, 构建新的红黑树
}
}
if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空
if (hc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点
// loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (loHead != null)
hiHead.treeify(tab); // 以hiHead为根结点, 构建新的红黑树
}
}
}
1,以调用此方法的节点开始,遍历整个红黑树节点(此处实际是遍历的链表节点,上文提过,红黑树节点也会同时维护链表结构)。
2,如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为0,则说明e节点扩容后的索引位置跟老表的索引位置一样(见下文例子1),进行链表拼接操作:如果loTail为空,代表该节点为第一个节点,则将loHead赋值为该节点;否则将节点添加在loTail后面,并将loTail赋值为新增的节点,并统计原索引位置的节点个数。
3,如果e的hash值与老表的容量(为一串只有1个为2的二进制数,例如16为0000 0000 0001 0000)进行位与运算为1,则说明e节点扩容后的索引位置为:老表的索引位置+oldCap(见例子1),进行链表拼接操作:如果hiTail为空,代表该节点为第一个节点,则将hiHead赋值为该节点;否则将节点添加在hiTail后面,并将hiTail赋值为新增的节点,并统计索引位置为原索引+oldCap的节点个数。
4,如果原索引位置的节点不为空:如果当该索引位置节点数<=6个,调用untreeify方法(见下文代码块11)将红黑树节点转为链表节点;否则将原索引位置的节点设置为对应的头结点(即loHead结点),如果判断hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)已经被改变,需要重新构建新的红黑树,以loHead为根结点,调用treeify方法(见上文代码块7)构建新的红黑树。
5,如果索引位置为原索引+oldCap的节点不为空:如果当该索引位置节点数<=6个,调用untreeify方法(见下文代码块11)将红黑树节点转为链表节点;否则将索引位置为原索引+oldCap的节点设置为对应的头结点(即hiHead结点),如果判断loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)已经被改变,需要重新构建新的红黑树,以hiHead为根结点,调用treeify方法(见上文代码块7)构建新的红黑树。
// 将红黑树节点转为链表节点, 当节点<=6个时会被触发
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null; // hd指向头结点, tl指向尾节点
// 从调用该方法的节点, 即链表的头结点开始遍历, 将所有节点全转为链表节点
for (Node<K,V> q = this; q != null; q = q.next) {
// 调用replacementNode方法构建链表节点
Node<K,V> p = map.replacementNode(q, null);
// 如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
if (tl == null)
hd = p;
else // 否则, 将尾节点的next属性设置为当前节点p
tl.next = p;
tl = p; // 每次都将tl节点指向当前节点, 即尾节点
}
return hd; // 返回转换后的链表的头结点
}
1,从调用该方法的节点,即链表的头结点开始遍历, 将所有节点全转为链表节点
2,调用replacementNode方法构建链表节点
3,如果tl为null, 则代表当前节点为第一个节点,将hd赋值为该节点,否则, 将尾节点的next属性设置为当前节点p
4,每次都将tl节点指向当前节点, 即尾节点
5,返回转换后的链表的头结点
总结
1,HashMap的底层是个Node数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
2,增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到key的hashCode值;2)将hashCode的高位参与运算,重新计算hash值;3)将计算出来的hash值与(table.length - 1)进行&运算。
3,HashMap的默认初始容量(capacity)是16,capacity必须为2的幂次方;默认负载因子(load factor)是0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
4,HashMap在触发扩容后,阈值会变为原来的2倍,并且会进行重hash,重hash后索引位置index的节点的新分布位置最多只有两个:原索引位置或原索引+oldCap位置。例如capacity为16,索引位置5的节点扩容后,只可能分布在新报索引位置5和索引位置21(5+16)。
5,导致HashMap扩容后,同一个索引位置的节点重hash最多分布在两个位置的根本原因是:1)table的长度始终为2的n次方;2)索引位置的计算方法为“(table.length - 1) & hash”。HashMap扩容是一个比较耗时的操作,定义HashMap时尽量给个接近的初始容量值。
6,HashMap有threshold属性和loadFactor属性,但是没有capacity属性。初始化时,如果传了初始化容量值,该值是存在threshold变量,并且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值作为新表的capacity值,然后用capacity和loadFactor计算新表的真正threshold值。
7,当同一个索引位置的节点在增加后达到9个时,并且此时数组的长度大于等于64,则会触发链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持。链表节点转红黑树节点的具体方法为源码中的treeifyBin(Node<K,V>[] tab, int hash)方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
8,当同一个索引位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的untreeify(HashMap<K,V> map)方法。
9,HashMap在JDK1.8之后不再有死循环的问题,JDK1.8之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
10,HashMap是非线程安全的,在并发场景下使用ConcurrentHashMap来代替。