闭眼背诵部分
它的底层实现 1.7版本的是数组+链表 为了解决极端情况下 hash冲突导致的获取键值效率低的问题
在 1.8 版本修改为 数组+链表+红黑树,这样的修改 可以使得在hash冲突严重时 获取键值的效率变高
以1.8版本来说 它的重要属性有 链表和红黑树互转的上限 8 和下限6,在
数组大于64 且 链表长度大于8 的时候 由链表转为红黑树
初始默认大小为16 默认负载因子0.75
hash 计算key 的方式:
根据key 来计算在数组中的位置时 使用到key的hashcode 此时会使用key的hashCode 异或自己的高十六位 与上
数组大小减一的值 来计算key在数组中的位置
扩容的时候 会扩容自己的2倍,在扩容时,旧的数组的位置要么是在原位置 要么是在原位置数加上原来的容量的位置
此时有一个巧妙算法,新增加的一位二进制数 直接与hashkey 就可以获得在新数组中的位置了。
源码解析
基础信息
HashMap<K,V> extends AbstractMap implements Map<K,V>, Cloneable, Serializable
//初始化默认值的大小 16 必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//最大的容量 2^31 - 1
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//转成红黑树的最少内部元素值 这个时候还会判断总元素大小大于64(在代码里找找)
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//这里再理解一下
static final int MIN_TREEIFY_CAPACITY = 64;
//单向链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//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() {
//键值对的 键hash 和值hash 进行与 相同0 不同1 ,最大程度保留 kv 特性
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) {//如果k 一样 且 v一样 则认为是一样(比较的还是内存地址)
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;// 这里 如果没有数据的话 在这里 resize 初始化
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//如果hash槽里没有数据 要新创建一个节点 next=null
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//如果当前槽里p的hash 和当前hash一致 p.key == key ; 则
e = p;//寻找到已经有这个key了 放在后面做value覆盖操作
else if (p instanceof TreeNode) //红黑树逻辑设值
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {//对p 的所有节点进行一个循环 查找 这个是一个无限循环 直到节点的子节点为空后退出
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//新建一个节点放到尾节点上
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 如果子节点数量达到8个 就转换成红黑树(binCount 从0开始 到7后 就有8个了)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//不存在下一个了 结束
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//初始化 或者扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //得到旧的map
int oldCap = (oldTab == null) ? 0 : oldTab.length; //得到旧的map的数组的容量
int oldThr = threshold;//得到旧的map容量扩容阈值
int newCap, newThr = 0;//定义新的容量 和新的扩容阈值
if (oldCap > 0) {//如果旧的map 有存储数据
if (oldCap >= MAXIMUM_CAPACITY) {//旧的容量已经达到最大值
threshold = Integer.MAX_VALUE;//将旧的扩容阈值加到最大
return oldTab;//无法扩容了 返回旧的map
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//如果旧的容量大于初始值 且旧的容量扩容一倍后小于最大值
newThr = oldThr << 1; // double threshold//给新的扩容阈值增大至旧的一倍
}
else if (oldThr > 0) // initial capacity was placed in threshold//没有初始化过 且扩容阈值被替换了 初始容量为扩容阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//空参的map会走到这里 给一个默认容量
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;//构造出新的map 覆盖当前的map
if (oldTab != null) {//如果旧的map 有值
for (int j = 0; j < oldCap; ++j) {//对旧的数组循环(每个数组内 可能是空 可能是链表 可能是红黑树)
Node<K,V> e;//定义一个新的头结点
if ((e = oldTab[j]) != null) {// 如果旧的数组当前有值 赋值给e
oldTab[j] = null;// 将旧的置空 等待gc回收
if (e.next == null)// 判断赋值过来的节点是否有子节点 只有本身一个
newTab[e.hash & (newCap - 1)] = e;//如果没有 结束掉 将e 放入新的数组对应的位置
else if (e instanceof TreeNode)//如果旧的迁移过来是一个红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 按照红黑树迁移
else { // preserve order//此处应该是存在小于8个的链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;//取出e的下一个节点//下面有精华 需要再理解《下面画图讲》
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;
}
// 如果子节点数量达到8个 就转换成红黑树(binCount 从0开始 到7后 就有8个了)查看下网络讲解
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();//这个时候为啥要初始化?需要确认一下
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null; //找到hash槽内的链表 循环链表 转成树
do {
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);
}
}
HashMap 为什么用红黑树 不用跳表
- 空间效率:红黑树的空间效率相对较高。跳表需要额外的空间来存储多级索引指针,这可能导致较大的空间开销。而红黑树只需要存储一个颜色属性,空间开销相对较小。在面向大量数据的场景下,空间效率是一个重要的考虑因素。
- 平衡性:红黑树是一种自平衡的二叉搜索树,它能够保证树的高度始终保持在O(log n)的范围内。这意味着红黑树的查找、插入和删除操作的时间复杂度都是O(log n)。而跳表的平衡性取决于节点插入时的概率分布,虽然在理论上跳表的平均查找、插入和删除操作的时间复杂度也是O(log n),但在实际应用中可能会受到概率分布的影响,导致性能波动。
- 实现复杂性:红黑树相对来说实现起来较为简单,而跳表的实现则需要考虑多级索引、节点插入概率等因素。此外,红黑树已经在Java库中实现,可以直接复用。这些因素使得红黑树成为HashMap中更为合适的选择
- 兼容性:在Java 8之前,HashMap采用链表来解决哈希冲突。红黑树可以看作是链表的升级版,因为它仍然保留了链表的结构(链表可以看作是退化的红黑树)。这种结构的兼容性使得将链表升级为红黑树成为一个相对容易的过程。