HashMap的底层实现是数组+链表+红黑树,JDK1.8之前的HashMap使用的是数组加链表,哈希函数取得再好也无法保证均匀分布,当哈希桶中有大量的数据的时候,HashMap就相当于一个单链表,时间复杂度为O(n,就失去了HashMap应有的优势,因此引入了红黑树,当哈希桶中的元素数量大于TREEIFY_THRESHOLD值时就转换为红黑树。
// 创建 HashMap 时未指定初始容量情况下的默认容量,默认16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// hashmap的最大容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// hashmap默认的负载因子是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 下面三个是HashMap中关于红黑树的三个参数
// 用来确定何时将链表转换为树
static final int TREEIFY_THRESHOLD = 8;
//用来确定何时将树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 当链表转换为树时,需要判断下数组的容量,当数组的容量大于这个值时,才树形化该链表;
// 否则会认为链表太长(即冲突太多)是由于数组的容量太小导致的,则不将链表转换为树,而是对数组进行扩容;
static final int MIN_TREEIFY_CAPACITY = 64;
①、treeifyBin(Node
//树形化函数
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果哈希表为空或者哈希表中元素个数小于进行树形化的阈值(默认64)时就去扩容数组长度
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//如果满足了树形化的条件,则进行树形化,e为指定位置桶里的链表节点,从第一个开始
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;//红黑树的头尾节点
do {
//新建一个树形节点内容和e一样
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)//确定树头节点
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)//让桶里的第一个节点指向新建的红黑树头节点,以后这个桶里的节点就是红黑树而不是链表了
hd.treeify(tab);
}
}
小结:上述方法做的事情如下:
上述函数前面部分实现的只是一个二叉树,而没有实现红黑树的操作,但在最后调用了hd.treeify(tab)
实现了构造红黑树,源码如下:
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {//第一次进入循环,确定头节点为黑色
x.parent = null;
x.red = false;
root = x;
}
else { //x指向树中的某个节点
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {//从根节点开始,遍历所有节点和当前节点比较然后调整位置
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);//比较哈希值
TreeNode<K,V> xp = p;
//把当前节点作为x的父亲,若x的哈希值比当前节点小则,x就是左孩子,否则为右孩子
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
由上可知,在二叉树转换为红黑树时要保证有序,上述函数有个双重循环,用树中所有节点与当前节点进行比较哈希值(如果哈希值相等,就对比键,这里不用完全有序),然后根据比较结果确定在树中的位置。
②、putTreeVal()
如果在添加元素时,相应位置已经是红黑树了,则需要调用红黑树添加元素的函数putTreeVal(),源码如下:
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;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;//如果哈希值和键值都相等就返回当前节点
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//如果当前节点和要添加的节点哈希值相等但键值不是同一个类,则挨个对比左右孩子
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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;//如果从ch所在的自述中可以找到要添加的节点,则直接返回
}
//哈希值相等,但键无法比较只好通过特殊方法给个结果
dir = tieBreakOrder(k, pk);
}
//上述操作得到了当前节点和要插入节点的大小关系
TreeNode<K,V> xp = p;
//判断当前节点有无左右孩子,没有左右孩子时才能插入,否则进入下一轮循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//插入后的平衡红黑二叉树的操作
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
//这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
//这里源码注释也说了,这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
由上面方法可知,当红黑树添加元素时的流程如下:
参考文章:Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构
有关红黑树的知识推荐阅读:
重温数据结构:深入理解红黑树
Java数据结构和算法(十一)——红黑树
其余方法源码见:散列表及HashMap简析