目录
1.HashMap
1.1整体架构
- 底层使用了数组+链表+红黑树的数据结构,当链表长度大于等于8会转换为红黑树,当链表长度小于等于6红黑树会转化为链表
- JDK1.7没有引进红黑树,单纯的使用链表解决冲突,在1.8版本中引进了红黑树并且实现了转换和退化操作
- 由于引进了红黑树所以,key的对象,必须正确的实现了Compare接口
//初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程中 HashMap 结构是否发生变化
transient int modCount;
//HashMap 的实际大小
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种情况
// 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方
// 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
//链表的节点
static class Node<K,V> implements Map.Entry<K,V> {...}
//红黑树的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
1.2新增源码解析
- 新增<K,V>元素
// 入参 hash:通过 hash 算法对key计算出来的值。
// 入参 onlyIfAbsent:false 表示即使 key 存在了,也会覆盖原来的值,默认为 false
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// n 表示数组的长度,i 为数组索引下标,p 为 i 下标位置的 Node 值
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组为空,使用 resize 方法初始化
//这里使用||和=等号就是为了给n进行赋值,然后在返回该值
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果当前索引位置是空的,直接生成新的节点在当前索引位置上
// n一般为2的次幂,计算速度快,相当于给hash值对n取模(得到值的范围为0~n-1)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 处理 hash 冲突
else {
// e 当前节点的临时变量
Node<K,V> e; K k;
// 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量
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 {
// 循环遍历链表
// e和p都是当前节点,只不过e是临时节点
for (int binCount = 0; ; ++binCount) {
// 遍历到达链表的尾节点
if ((e = p.next) == null) {
// 把新节点放到链表的尾部
p.next = newNode(hash, key, value, null);
// 当链表的长度大于等于 8 时,链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 遍历中,发现有元素和新增的元素相等,结束循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//更改p指针,当前节点
p = e;
}
}
// 没有到达尾部,有相同的的节点
if (e != null) {
V oldValue = e.value;
// 当 onlyIfAbsent 为 false 时,才会覆盖值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回老值
return oldValue;
}
}
// 更新版本号
++modCount;
// 新增完成后进行扩容,
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null; //没有重复的元素值被覆盖就返回null
}
首先把哈希值对n进行取模计算索引位置,判断是否有哈希冲突
如果有冲突,就判断节点是链表还是红黑树(是红黑树就直接进行插入节点,是链表就从前向后遍历,如果没重复元素就会遍历到尾部进行新增一个节点,并判断是否长度大于等于8转化为红黑树,如果有重复就遍历到此处,然后进行判断是否可以覆盖,默认是可以覆盖的,并返回旧值)
如果没冲突就新增一个节点即可
最后进行更新版本号,并且判断是否需要进行扩容
- 新增链表节点,链表节点的新增就是尾部追加(可能会进行更新值),最后在判断是否长度大于等于8
ps:由于链表长度小的时候虽然是线性时间但是时间的常系数小相比于红黑树的log复杂度的系数,所耗费的时间要少,8是这个选取的临界值;而且链表的长度为8的时候一般只会是负载因子选取不当,一般正常编码几乎不会出现这种情况
- 新增红黑树节点,会递归的把当前节点与新增的节点进行比较,直到当前节点为叶子节点,进行新增和着色(如果元素实现了Compareable接口,就根据此来比较,如果没有实现,就使用equals方法),最后进行判断树是否平衡
ps:不管是链表新增还是红黑树新增,里面都包含了查找定位功能,链表查找(新增链表节点的过程中判断值是否相等,如果到达尾部就会返回null),红黑树查找(新增红黑树节点的过程中就会判断是否值相等,如果到达叶子节点就会返回null)