HashMap 1.8 源码个人解读
文章目录
该篇文章为个人阅读源码后的理解,有错误还请指点。本文的图源自链接 5 。
参考文章:
HashMap 简介
HashMap<K,V> 继承自 AbstractMap 还实现了 Map<K,V>,Cloneable,Serializable 的接口。内部存在一个静态类 Node 与 TreeNode 是需要知道的,Node 是存储数组上链表的单个节点对象(JDK1.7 中为 Entry),TreeNode 是 JDK1.8 新加入的 红黑树存储结构(本编文章不对红黑树介绍)。
HashMap 是基于拉链法(链地址法)实现的一个散列表,内部由 数组 + 链表 + 红黑树 实现。hashmap 在存储为 null 的主键时会固定的存放在数组下标 0 的位置上,这点上注意在线程安全的 ConcurrentHashMap 是不能存储 null 主键的。
HashMap 数据与存储结构
从下图中可见 数组、链表、红黑树 3个关键字,其中链表长度大于 8 (TREEIFY_THRESHOLD)时转换为红黑树其实只是尝试进行转换,具体会不会真实转换还得看 table 的长度是否达到了 64 (MIN_TREEIFY_CAPACITY)位,而且如果当红黑树的节点缩小至 6 (UNTREEIFY_THRESHOLD)时还会从红黑树化解为链表的结构,这些都是 HashMap 作者测试出比较平衡的几个阈值大小。
接着说说 HashMap 的存储步骤,首先在 put 时要取得数组 table 的下标,大致可以分为这 3 步:取 key 的 hashcode -> 高位运算 -> 取模计算。那么问题来了,很多人只知道 h & (table.length -1)
,但是却不明白高位运算是什么意思,这个其实也可以称为 “扰乱算法”,这个将在后面的 hash() 方法解析中说明。
hash 算法介绍
Hash 就是把任意程度的输入,通过 散列算法 变换成固定长度的数据,该输出值俗称 “散列值”,所以 Hash 表也称为 散列表。但是输入值不同,不代表转换后的 “散列值” 不同,不同的输入值,是有概率会产生相同的散列值的,这种现象可以称作 “碰撞”(hash 冲突也是这么来的),不过不同散列值它们的输入值肯定不同。
具体的 散列算法,可以在头部 相关链接2 中进行详细了解。
在 HashMap 中的主要作用就是根据对象的 hashcode 进行高位运算的 2 次加工,但是要注意的是,如果重写了 equals() 方法,就必须将 HashCode() 方法也进行与 equals() 相同比较逻辑重写。因为在 HashMap 调用 get() 方法时,先是按照 h & (length - 1)
来进行寻址的,也就是 [哈希码 & ( hashmap长度 - 1 )] 方式获取的下标,当找到元素位置后的链表遍历则是 通过的 equals() 方法进行的比较,可想而知,如果你 hashCod 的得出的整数,方法没有进行重写,而是按照 Object 基类的内存地址转换来寻找,然后 equals 链表比较时又按照了你重写的逻辑寻找,将不是原来的 Object 基类的 “==” 内存地址比较逻辑,(object 的 equals 是比较的地址) 那么结果肯定将会出现与你期望的效果不一致。
在 HashMap 的 hash 算法中返回值会再次将对象的 hashcod 进行 扰动计算,降低 hash 冲突的概率。在 jdk1.8 中扰动计算有所改动,由 jdk1.7 的 4 次 右移异或混合 改为 16 次。
具体的 扰动计算,可以在头部 相关链接2 中进行详细了解。
public class test {
public static void main(String[] args){
TTT t1 = new TTT("张三",1);
TTT t2 = new TTT("张三",2);
Map map = new HashMap();
map.put(t1,t1);
// 我这里按照业务想根据 name 去寻找,本以为只要重写 equals 就行了,
// 但是没有重写 hashCode,他们两的名字明明都是相等的,但是就是找不到
// 因为 Object 的 hashCode 是按照对象本身的内存地址转换的 哈希码
// 然后根据 哈希码 通过 indexFor 来寻址找到的下标
System.out.println(map.get(t2));
}
}
class TTT{
String name;
Integer id;
public TTT(String name, Integer id) {
this.name = name;
this.id = id;
}
@Override
public boolean equals(Object obj) {
TTT t1 = (TTT) obj;
System.out.println("调用了 equals 方法" + obj);
if(this.name.equals(t1.name)){
return true;
}
return false;
}
@Override
public String toString() {
return "ttt{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
// @Override
// public int hashCode() {
// return this.name.hashCode();
// }
}
Node 链表的存储对象
Node 类是实现的 Map.Entry 接口,在 Node 类中,有 4 个成员属性分别是 hash,key,value,next ,方法为 Node(),getKey(),getValue(),toString(),hashCode(),setValue(),equals(),在这些方法中,hashCode() 计算是取 key 的 hashcode 和 value 的hashcode 进行 ^ 异操作,并调用了 Objects 的 hashCode 的非空判断方法。
在 setValue() 方法中,除了替换之前的 value 值,还会返回老的 value 值。在 equals() 方法中会判断传进来的值是否为 Map.Entry 的类型,这个类型是接口套接口的用法,很有意思,然后才会比较调用自身的 key 和 value 的 equals() 方法与传进来的进行比较。
额外补充:
hashmap 的 TreeNode 红黑树是属于 Node 的 孙类,Node 的子类为 LinkedHashMap.Entry ,然后由 TreeNode 继续继承了 LinkedHashMap.Entry。
// 1.8
static class Node<K,V> implements Map.Entry<K,V> {
// 存储高位移动计算后的 hash 值
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;
}
}
成员属性与静态常量
另外在 HashMap 源码中,还存在着很多的 static final 静态常量作为阈值的存在
例如:
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认大小 16
- static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子
- static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的长度阈值
- static final int MAXIMUM_CAPACITY = 1 << 30; // hashmap 的最大扩容
- … … 其他了解的常量 ↓
- static final int MIN_TREEIFY_CAPACITY = 64; // 用来判断是否真的需要转红黑树的阈值
- static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转链表的阈值
重要成员属性:
- transient Node<K,V>[] table; // 存储所有节点的数组,也可以叫做 哈希桶数组
- transient Set<Map.Entry<K,V>> entrySet; // 缓存相关的,没有了解
- transient int size; // 实际存储的元素数量(实际存储于数组长度分开理解)
- transient int modCount; // 记录内部结构发生变化的次数,put操作(覆盖值不计算 fail-fast机制)
- int threshold; // 允许的最大的存储的元素数量,通过 table.length*loadFactor 增长因子得出
- final float loadFactor; // 数据的增长因子(负载因子),默认为0.75。在进行扩容操作会使用到。
fail-fast 机制:
方法解析
static - hash()
这个方法在 jdk1.7 的时候会参差两三行的 扰乱算法,但是在 jdk1.8 后 改良 了一些,因为加入了 红黑树 就不必要在像以前那样节省作用不大的高低位位移的 cpu 资源了,用来将资源提供给红黑树计算,性能比会更高。
高低位位移是为了二进制后的数 高位影响低位,移位到后面 与 table.length - 1 的值去&计算数组的准确下标位置,使一些比较规则的 hashcode 值(包括开发者重写后后的 hashcode() 的方法)更具有 散列性,能更 均匀的分布。
// 1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
科普:
实际上java中%符号是求余(rem),而不是取模(mod)。在除数和被除数都为正数时,取模和取余的结果是一样的,区别在于有负数出现的时候。所以我们容易混淆这两个概念,而在某些编程语言中%符号代表取模,所以很难区分它的叫法,我们知道%在自己使用的编程语言中代表什么就行了。
123 / 8
// 用移位运算可以表示为右移3位:
1111011 >> 3
余数正是被移位运算移走的最低3位,011,也就是余数为3。
原来 移位运算移走的那些二进制就是余数!
N % M == N & (M - 1)
static - tableSizeFor()
这是将目标参数转换为 最接近 => 的 2 次方数 的方法,主要用途就是用于将一些不确定的值转换可控范围的值,可以用来 降低 hash 冲突 的情况,最容易理解的就是在 hash 带参构造中将使用者传入的 容器大小 转为 => 2 次方数的值,原理就是二进制的 补位 操作,Integer.highestOneBit
方法,也是运用的这个原理。
// 1.8
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}
new - HashMap()
以下图为 帮助文档 中展示的 hashmap 所有构造方法,在用户传入 initialCapacity < 0 或 loadFactor <= 0时将会抛出异常,并且 initialCapacity 的初始容量大小也不能超过阈值 1<<<30(1073741824)的大小,否则只会按照最大值(MAXIMUM_CAPACITY) 设置,这个值是符合 2 的幂次方规则,超过这个值后的 hashmap 将不会扩容,只会继续在链表或红黑树上继续添加元素。
另外 hashmap 的作者设计时为了考虑恒定性能,选择的 16 与 0.75 也是经过计算测试过的,提供为了空间与时间的良好的折中,初始容量和负载因子是比较影响性能的两个重要参数。
补充:
因为 integer 的 MAX_VALUE 最大也不过是 2147483647 ,也就是 2 的 32 次方 -1 的大小,所以 1 << 30 其实也就是 2 的 29 次方,如果再次按照扩容机制的 2 倍扩容将会 = 2147483648,也就是超出了 integer 的最大值大小。
// 1.8 的有参构造
public HashMap(int initialCapacity, float loadFactor) {
// 容量大小不能为 0,如果大小大于了最大阈值 1<<30 的话就 = MAXIMUM_CAPACITY
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 自定义的负载因子浮点型只能为 > 0 的数
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 这里注意调用了这个方法,是将容量调整到 往上取最接近 2 次方的值,例如 7 -> 8, 10 -> 16
// 在第一次 put 操作,扩容数组时,会将这个 threshold 作为数组有参构造的出事容量,
// 然后重新计算这个值 按照 table.length * loadFactor
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
// 默认 0.75 负载因子调用 上面的带参构造
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 1.8 的无参构造
public HashMap() {
// 可以看出在无参构造中并不会初始化很多的属性,只是设置了 0.75 的负载因子
// 而其他 int 都是默认 0 数组则是 null,在后面扩容时分支会不同
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
object - get()
获取元素的方法,主要调用了 getNode() 方法的逻辑,并返回出去。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
object - getNode()
获取元素的封装起来的具体逻辑代码。
// 1.8 手动格式化后的容易理解的样貌
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab = table;
int n = tab.length;
Node<K,V> first = tab[(n - 1) & hash];
Node<K,V> e = first.next;
K k = first.key;
// 如果数组不为 null,并且长度 > 0,
// 并且通过 hash() 方法转换后的 key 值与 长度 - 1 方式相 & ,
// 得出准确下标(与 1.7 indexFor() 方法相似)判断不为空
if (tab != null
&& n > 0
&& first != null) {
// 如果头元素的 hash 与 传进来的 hash 相同,并且 key 也是相同的(== 和 equals 满足1个)
if (first.hash == hash && // always check first node
(k == key || (key != null && key.equals(k))))
// 说明第一个元素就是直接返回出去
return first;
// 如果 e 也就是下一个元素不为 null 则进入
if (e != 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;
}
object - containsKey()
判断某个键是否存在,与 get 相同,也是调用的 getNode() 方法。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
object - put()
将 key 根据 干扰计算 后,并传入核心逻辑代码进行添加。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
object - putVal()
图解 1.8 的 put 具体流程:
源码解析:
// 1.8 手动格式化后的容易理解的样貌
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab = table;
Node<K,V> p = tab[i = (n - 1) & hash];
int n = tab.length;
int i; // 这个 i 没被使用过,可能作者忘记使用了
// 如果数组还没有初始化使用过,则进行一次初始化,hashmap 在构建时是不会初始化的,除非使用时才初始化数组
if (tab == null || n == 0)
n = (tab = resize()).length;
// 如果根据计算好的下标找到的节点依然是 null 的,则直接赋值添加
if (p == null)
tab[i] = newNode(hash, key, value, null);
// 进入这里代表需要遍历链表或者红黑树了
else {
Node<K,V> e;
K k = p.key;
// 判断第一个元素是不是重复的元素,是的话则直接赋值给 e 不用遍历
if (p.hash == hash &&
(k == key || (key != null && key.equals(k))))
e = p;
// 判断是不是 红黑树结构,成立则直接按照红黑树方式添加值,并赋值给 e
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则就是链表结构,直接进行循环遍历即可
else {
for (int binCount = 0; ; ++binCount) {
// 循环里的 e 不断迭代 .next
// 如果迭代为 null 则直接创建一个新节点,添加在尾元素中,尾插方式,1.7 为头插
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表添加成功后,判断是否达到 8 的阈值,如果达到则将该链表转换为红黑树的结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 不断判断是否存在于链表身体中,存在则直接结束循环,break 掉,此时的 e 则是目标元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 迭代更新
p = e;
}
}
// 最后判断 e 是否为 null,存在则代表着是被 覆盖掉,这里会将老的值返回给使用者
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这里在 put 调用时固定了 onlyIfAbsent = false,始终成立,
// 就是将传入新的 value 覆盖掉老的 value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 方法体为空,在 LinkedHashMap 中有被重写
afterNodeAccess(e);
// 返回老的 value
return oldValue;
}
}
// 如果不是覆盖添加,是添加了一个新的元素,则添加次数 ++,这是为了多线程并发做的准备
++modCount;
// 如果添加后的实际元素大小 > 了最大的阈值则扩容
if (++size > threshold)
resize();
// 方法体为空,在 LinkedHashMap 中有被重写
afterNodeInsertion(evict);
return null;
}
object - resize()
在 1.7 中扩容后会有重新 rehash 的操作,但是很难执行那个 if ,一般都是与 1.8 一样的扩容原理,所以在 1.8 中直接废除了 rehash 的判断,在 1.7 中的扩容机制会使得链表出现 倒置的效果!!!1. 7 总所周知的知道 头插 会使得程序在多并发容易出现环形链表的情况,所以在构造时如果知道自己使用的大小,可以直接固定大小和负载因子,从而避免扩容的操作,另外负载因子设置为 1 可以节省内存,但是会有减低性能的效果。
只有在下标节点只有一个首元素时才会重新 hash 计算一个新坐标位置,否则就按照容量的 2次方 bit 头判断相与 = 0 和 else 的情况,符合 if 条件的将会放在原地,进入 else 的会添加在 [下标 + 老数组长度] 的位置上,这也是 1.8 的改良不错的地方。
为了方便理解上面的 hash 扩容机制,以下图举个例子,初始容量为 16 ,key 1 和 key 2 是存储在 Node 节点上计算好的 hash 码值,在 a 中,可以看出 key1 和 key2 相与运算后都是在 101 在下标 7 上的。在扩容为 32 容量后的 b 中,他们其实只需要看 n -1 的首位元素 1 和 key 的hash 相与运算,能否 = 0 就好了,具体结果再看 第二张图。
可以看出相与运算后 != 0 的情况刚好是 原位置下标 + 老数组长度 得到的总和 转换为 2 进制的数字。
源码分析:
final Node<K,V>[] resize() {
// 扩容前将目前的信息作为 老old 变量备份一份
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 长度
int oldThr = threshold; // 最大可允许的存储元素 无参构造时默认值为0
int newCap = oldCap << 1;// 扩容后的长度,等同于 老长度 * 2,用 << 效率更高
int newThr = 0;
// 如果老的长度 > 0 则代表不是第一次扩容,是 put 时进行的扩容,table != null,
if (oldCap > 0) {
// 判断长度是否已经达到了最大扩容阈值 1<<<30,如果达到了则将 threshold 也设置为最大容量,
// 这里的最大容量使用了 Integer.MAX_VALUE 就代表着所有空间都可以使用,
// 而不需要 .length * 负载因子的方式限制阈值了,结束后并返回了老的 table ,因为没有改变
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 或者如果扩容后的长度符合最大扩容的阈值范围,并且 老长度 >= (1<<4) 就将阈值也进行相对应的位移 1 位
// 也就等同于 newCap * threshold ,使用 << 方式更效率
else if (newCap < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 进入这里代表着 table == null,代表着有参构造出的 hashmap,在有残构造中 oldThr = 的 threshold
// 无论如何都是为 >0 的,是通过 tableSizeFor() 方法得出的,即是负数或0 都会最终成为 1
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 无参构造时因为没有给可用元素 threshold 赋值,所以默认为 0 ,而且 table == null,所以进入这里
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;// 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 16 * 0.75
}
// ... 可能是上面newThr = oldThr << 1时,最高位被移除了,变为0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新的可用容量复制给成员属性 threshold 上
threshold = newThr;
// 根据得出的新数组长度创建一个新 Node 数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果 老的数组不是 null 了,即代表初始化后了
if (oldTab != null) {
// 开始遍历 Node 数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e = oldTab[j];// 获取元素
if (e != null) {
oldTab[j] = null;
// e.next == null 则代表只有第一个元素
if (e.next == null)
// 直接进行重新的下标运算然后设置上去,这里的 Node e 的 hash 是之前干扰算法计算存储过的
newTab[e.hash & (newCap - 1)] = e;
// 判断是否为 红黑树 结构
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;
// 直接以老数组长度 2 的次方数(不是 - 1) 与 hash 码与运算,此时只会出现两种情况
// 因为 2 的次方数尾巴都是 0 结尾,只要判断头的 1 与 hash 码对应的数比较即可
// 是0的话索引没变,是 1 的话索引变成 “原索引+oldCap”
if ((e.hash & oldCap) == 0) {
// loHead 与 loTail 如同两个指针,一个指向链表的头一个指向尾
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);
// 循环结束后会分出两条链表结构,一个是 = 0 的能够继续查找到的,
// 另一个是 != 0 的可以在扩容后的数组长度找到的,一会将有图解
if (loTail != null) {
loTail.next = null;
// 继续放入原始位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 放入 [ 下标 + 老长度 ] 的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新的表格
return newTab;
}
object - treeifyBin()
// 1.8
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组容量太小 <64 则不进行转红黑树,会继续选择扩容 resize();
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// MIN_TREEIFY_CAPACITY = 64
resize();
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)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
总结:1.7 与 1.8 的 hashmap 区别
- 红黑树 -> TreeNode 的加入 (数组 + 链表)变为(数组 + 链表 + 红黑树)复杂度从O(n)变成O(logN)提高了效率)
- Node -> 与 JDK 1.7 的对比(Entry类),仅仅只是换了名字
- 静态常量 -> 加入了 红黑树相关的阈值静态常量,Node 长度 >= 8-1 尝试转红黑树,table 大小 > 64 转换红黑树,Node 长度 <= 6 由树变链表
- 扰动处理 -> 高位运算更精简, jdk1.7 的 4 次 右移异或混合 改为 16 次
- put 存储 -> 新增后进行长度判断尝试转红黑树的分支
- resize 扩容 -> 头插变尾插,去除 rehash 判断,并使用了 新增参与运算的位 ==0 和 else 的方式进行转移重新存储在 [下标 + 老数组长度] 的位置
列出两张 1.7 与 1.8 的扩容图解,可以简单的看出一个在扩容后变成了倒序,而 1.8 则不是这样,倒序是因为使用的头插方式,并发情况变为环形链表也有这个因素,在 1.8 变为尾插 改良了很多。
1.7 在扩容后的图解:
1.8 在扩容后的图解: