在下文中,你将学到:
- HashMap源码的各个参数和方法的含义以及简要原理
- HashMap的为什么要选用红黑树作为他的扩容方式,为啥不是AVL,二叉搜索树等,他们的优缺点比较
- 带你利用java纯手写一个红黑树
一.HashMap的特点(数组 + 链表 + 红黑树)
---- 1.HashMap继承于如下接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
---- 2.一个最简单的Hashmap构成:数组 + 链表
ps:我个人喜欢叫这种哈希表为拉链法
那么问题来了:
1)问题一:既然是数组,那么数组初始化了,那么申请的内存有多少,初始化了多大的长度?
源码就是天然的技术文档,看源码喽。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 1左移4位,变为1000,初始化为16
2)问题二:这个数组可以申请的最大长度是多少呢?
static final int MAXIMUM_CAPACITY = 1 << 30; // 数组初始化的最大长度接近int的临界值1 << 31
ps这里有这么一个常量是啥意思呢:static final float DEFAULT_LOAD_FACTOR = 0.75f; // 缺省的负载因子的大小
负载因子?
负载因子是用来计算负载容量(所能容纳的最大Node个数)的,当前list长度 length,负载因子 loadFactor
负载容量的计算公式为:
threshold = length * loadFactor
默认负载因子为 0.75。也就是说,当Node个数达到当前list长度的75%时,就要进行扩容,否则会增加哈希碰撞的可能性。负载因子的作用是在空间和时间效率上取得一个平衡。
float DEFAULT_LOAD_FACTOR = 0.75f
3)问题三:上面提到了红黑树,那什么时候用红黑树呢?
首先我们要知道为啥要使用红黑树,如果采用链表可能会出现如上情况,当我们插入数据的时候会通过计算它的hashcode之后把它插入到某个数组节点下方,然后一直拉链。导致这个拉链很长很长,然后你在拿这个值的时候就会变为On的复杂度,违背了hashmap接近O1复杂度的原则,于是乎,红黑树诞生了。红黑树的优势在哪咱们在下面讲。
---- 这里又有一个问题:链表啥时候会转换成红黑树?
static final int TREEIFY_THRESHOLD = 8; /**树化阈值*/
static final int MIN_TREEIFY_CAPACITY = 64; /**所有的元素超过64, 链的长度超过8,会tree话*/
源码里面说的很明确,当hashmap里面所有元素超过64个,某个链的长度超过8,此时才会tree化
---- 注意当你删除hashmap里面的元素,导致单颗树的元素 <= 6时,此时单颗树就会再一次变为链表
static final int UNTREEIFY_THRESHOLD = 6;/**树降级成为链表的阈值*/
4)问题四:上图中链表的结点的构成是啥呢?
Node(int hash, K key, V value, Node<K,V> next) { // 链表的基本知识
this.hash = hash; //保存当前节点的hash值
this.key = key; // key,value保存结点信息
this.value = value;
this.next = next; // 保存结点的下一个结点的位置,以达到拉链的目的
}
5)问题五:hashmap的初始化数组的方法
/** 作用:返回一个大于等于当前cap的一个数字,并且这个数字一定是2的次方数* cap = 3 = 11
* 3 - 1 = 2 = 10
* 10 >>> 1 = 01 | 11 = 11 ;11 >> 2 = 00 | 11 = 11;11 >> 4 = 00 | 11 = 11;
* 11 >> 8 = 00 | 11 = 11;11 >> 16 = 00 | 11 = 11;
* 11 + 1 = 100 = 4 所以其实真正的初始化的数组长度为4
* 所以 数组初始化的时候其实就是把cap的二进制位都变为0然后左移一位拿到初始化长度
static final int tableSizeFor(int cap) {
int n = cap - 1; /**不减一的话会得到一个想要的数 * 2的一个大小*/
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;
}
6)问题六:hashmap的put方法,get方法, remove方法
ps:下文提到了哈希的计算,这里提前介绍一下hash的计算(扰动函数)
* 作用: 让key的hash值的低16位与高16位参与运算 */
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
右位移16位,正好是32bit的一半(int
是32位的),自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也变相保留下来。
public V get(Object key) {
Node<K,V> e;// 存的时候hash了一下,所以这里要拿到hash之前的数据
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {// tab 引用当前hashMap的散列表
// first :桶位中的头元素
// e:临时node元素
// n:table数组长度
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 第一种情况:定义出来的桶位,即为咱们要get的数据
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 说明当前的同为不只一个元素,可能是链表也可能是红黑树
if ((e = first.next) != null) {
// 第二种情况,桶位升级成了红黑树
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))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
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) {
/** * tab 引用当前hashmap的散列表
* p 表示当前散列表的元素
* n 表示散列表数组的长度
* i 表示路由寻址 结果
* * */
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中最耗费内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//最简单的一种情况,寻址找到的位置刚好是null, 直接将当前节点扔进去就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// e:node临时元素(不为null,找到一个与当前要插入的key val 一致的key)
// k:表示临时的一个key
Node<K,V> e; K k;
// 表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) // 红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表的情况,而且链表的头元素与我们要插入的key不一致
for (int binCount = 0; ; ++binCount) {
// 说明迭代到最后一个元素了,也没找到一个与你要插入的key一致的node,
// 说明要加入到当前链表的末尾
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的node元素,需要进行替换操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e不等于null, 条件成立, 找到了一个与你插入元素key完全一致的数据,需要进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// modCount 表示散列表结构被修改次数, 替换Node元素的value不计数
++modCount;
// size表示散列表中node数量, 插入新元素,size自增,如果大于阈值,触发自增
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab:引用当前hashMap中的散列表
// p:当前node元素
// n:表示散列表数组长度
// index:表示寻址结果
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) { // p计算hash位置
// 说明路由的桶位是有数据的,需要进行查找操作,并且删除
// node表示查找到的结果
// e:表示node的下一个元素
Node<K,V> node = null, e; K k; V v;
// 第一种情况,当前桶位中的元素即为你要删除的元素
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) // 判断当前桶位是否升级为红黑树了
// 第二种情况
// 红黑树的查找操作,看我的红黑树的教程
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 第三种情况
// 链表的情况
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 判断node不为空的话,说明按照key查找到需要删除的数据了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 第一种情况:node是树节点,说明需要树节点移除
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;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
7)问题七:为什么要扩容?是不够放了吗?No,一个很重要的原因是为了解决哈希冲突导致的拉链化严重,导致查询变为On,扩容可以解决该问题
final Node<K,V>[] resize() {
// oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
// oldCap:表示扩容之前的table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:表示扩容之前的扩容阈值,触发本次的扩容的阈值
int oldThr = threshold;
// newCap: 扩容之后的table大小
// newThr:扩容之后,下次再触发扩容的条件
int newCap, newThr = 0;
// 条件如果成立说明 hashMap中的散列表已经初始化过了,这是一次正常的扩容
if (oldCap > 0) {
// 扩容之前的table数组大小已经达到 最大阈值了,则不扩容, 扩容条件设置为int最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// oldCap左移一位实现数值翻倍,并且赋值给newCap 小于数组最大值限制且扩容之前阈值 >= 16
// 这种情况下,则下一次的阈值等于当前的阈值翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} // oldCap = 0,说明hashMap中的散列表是null
// new hashMap(initCap, loadFactor);
// new Hashmap(initCap);
// new HashMap(map); 并且这个map有数据
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr; // 说明newCap一定是2的次方
// new HashMap() oldcap = 0, oldThr = 0
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75 * 16 = 12
}
// 传递的小于16的时候就为0了, newThr为0时,通过newCap和localFactor计算出一个newThr
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;
// 说明hashMap本次扩容之前,table不为null
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
// 当前Node节点
Node<K,V> e;
// 说明当前桶位中有数据,但是数据具体是单个数据,还是链表,还是红黑树,并不知道
if ((e = oldTab[j]) != null) {
// 方便JVM GC时回收
oldTab[j] = null;
// 说明没有拉链,直接根据哈希值和数组长度计算出当前元素的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 第二种情况:当前节点已经tree化
else if (e instanceof TreeNode)
((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;
Node<K,V> next;
do {
next = e.next;
// hash -> .... 1 1111
// hash -> .... 0 1111
// & 1 0000
// 如果链表中的高位与1 得1 则放在高位链中
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}