前言
通过学习jdk1.7版本的HashMap源码我们知道,jdk1.7中HashMap的transfer方法在并发条件下容易产生死循环链表的问题,并且 在某些情况下,一条链表下挂载着许多节点,查找效率很低。此外,HashMap的put方法采取头插法的方式将新的节点插在链表头部,这使得HashMap扩容后发生链表元素逆序。在jdk1.8版本中,java开发团队对HashMap进行了一系列优化,提高了HashMap的性能。
在jdk1.8中,HashMap底层实现采用的是数组+链表+红黑树的数据结构,在链表长度达到一定的阈值时,可能会将链表转换成红黑树,将查找操作的时间复杂度由O(N)优化到O(logN)。其put方法将节点插入链表时改换成尾插法,在扩容后链表元素依然保持原来的顺序。
为什么采用红黑树?既然红黑树是一种平衡二叉树,那为什么不使用AVL树?
因为:使用红黑树不仅可以提高HashMap的性能,而且可以作为HashMap处理哈希冲突的另一种策略。jdk1.7版本的HashMap当某条链表中的元素数量太多时,增删改查效率低,HashMap性能不高。而红黑树的优点是增删改查效率较高,引入红黑树这种数据结构后,可以有效提高性能。当链表的长度到达一定值时,可以将链表转换成红黑树(链表与红黑树的转换条件见后面详细描述)。同时,当发生哈希冲突时,若当前table[index]位置是一颗红黑树,那么可以通过将该节点插入到红黑树来解决。
红黑树实际上不是”完全平衡“的二叉树,红黑树相比AVL树最多不平衡一层,相当于查询的时候最多只比AVL树多一次比较,但是红黑树增删节点性能较高,红黑树增加和删除节点时不一定像AVL树一样经过多次旋转以及平衡计算来维持平衡二叉树。
jdk1.8版本下的HashMap依然是线程不安全的,在高并发场景中推荐使用java.util.concurrent包下的ConcurrentHashMap来保证线程安全。
底层实现
HashMap节点的具体实现
//普通节点,和jdk1.7HashMap中的Entry是一样的
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
//树节点,TreeNode--->LinkedHashMap.Entry<K,V>------>HashMap.Node<K,V>
//由此可见,树节点和普通节点差不多,但是树节点占用的空间比普通节点大很多
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
成员变量/常量
==大部分与jdk1.7中的相同==
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*
将链表转化为红黑树的阈值
面试题:为什么这个阈值是8?为什么不是7,或者20?
参考源码中的注释:
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins. In
usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
解释:
一个红黑树节点的大小比普通链表节点大,所以要尽量在链表的长度达到一定值时,才将其转化为红黑树.但是,当HashMap进行remove或resize操作使得红黑树中节点的数目变小时,又会将红黑树重新转换成链表.所以,在使用散列性较好的hash算法时,应该要较少使用红黑树.
根据概率学研究,在理想情况下经过哈希散列,table数组每一个桶中的节点个数遵循参数为0.5的泊松分布.
可以发现:桶中存在8个节点的概率为0.00000006,而大于8个的概率更是小.
如果阈值设为7,树节点占用空间比普通节点大,红黑树转化为链表的操作太频繁;如果设置为20,链表太长,链表性能低;
所以,链表转化为红黑树的阈值为8.
*/
static final int TREEIFY_THRESHOLD = 8;
/*
将红黑树转换为链表的阈值
为什么为6,而不是7或8?
因为:链表与红黑树之间的转换十分复杂,应该尽量避免频繁的相互转换.
当链表长度大于8时可能就会转换成红黑树了,若红黑树转换成链表的阈值太接近,就会造成红黑树与链表频繁的转换.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/*
链表转换为红黑树所要求的数组table长度的阈值
注意:链表转换成红黑树的条件有两个:
1、链表的长度大于8
2、数组的长度大于等于64
如果条件2不满足,则会对HashMap进行扩容(见resize方法)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中的数组
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
/*
负载因子,与扩容阈值相关,threshold=capacity*loadfactor
负载因子太小,查找性能高,空间利用率低,哈希冲突可能性小,扩容操作频繁
负载因子太大,空间利用率高,链表元素多,哈希冲突可能性高,查询效率低
默认值0.75是在 查找效率 和 空间利用率 之间均衡之后得到的结果
所以一般不手动设置负载因子,采用默认的0.75
*/
final float loadFactor;
/*
HashMap扩容阈值
**注意:jdk1.7和jdk1.8中HashMap的扩容条件不一样
jdk1.8中只要HashMap元素个数size大于threshold,就需要进行扩容.**
*/
int threshold;
方法
构造方法
/*
在HashMap中会为我们创建一个长度为大于initialCapacity的最小2的整数次幂的数组
如果传入的initialCapacity不合适,可能导致扩容操作很频繁
在《阿里巴巴Java开发手册》中建议:
当我们明确HashMap中要存储的元素个数时,initialCapacity可以设为:
initialCapacity = (需要存储元素个数/负载因子) + 1
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//根据传入的initialCapacity,求得一个比它大的 最小2的整数次幂
/*
为了防止为创建数组分配了内存空间,但是HashMap没有被使用的浪费空间的情况
HashMap采取的是延迟加载机制
构造方法中并未创建一个长度为2的整数次幂的数组
而是将这个2的整数次幂先暂时存在threshold中
在第一次进行put操作时才会创建数组
*/
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//注意无参构造函数并没有在内部调用具有两个参数的构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
tableSizeFor方法
求大于给定cap的最小2的整数次幂
//该方法也是获得一个数count,该数符合:
// 1、count = 2的幂次方.
// 2、count >= iniCapacity
//见jdk1.7HashMap中的roundUpToPowerOf2方法进行比较
static final int tableSizeFor(int cap) {
//如果不-1的话,就会发生:传入的8,却返回了16的情况
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;
}
hash方法
根据key计算hash值
static final int hash(Object key) {
int h;
/*
将hashcode值h与其高16位进行按位异或运算
如果数组长n很小,用hash值和n-1进行按位与操作时,求得的index就会只与hash值低位有关
如果hash值的高位变化很大,低位变化很小,就会很容易发生hash冲突
该hash算法将hashcode的高位利用起来,可以避免这种情况
*/
/*
经过hash算法返回的hash值会用于与tab.len-1进行运算从而计算元素放在数组位置的下标
在hash算法中,jdk1.8的hash方法进行了两次扰动处理(右移一次、异或一次)
扰动处理的目的是使得计算出的hash值低位尽可能地随机、均匀,从而可以减少hash冲突的概率
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put方法(重要)
向HashMap添加节点。如果存在key相同的节点,则用传入的value覆盖oldValue,并返回oldValue
如果不存在,则插入节点,返回null。
put方法流程:
1、判断table是否被初始化,没有则调用resize方法初始化
2、根据index=hash&(table.len-1),求得index
3、是否发生哈希冲突
3.1没有发生哈希冲突,即table[index]==null,则table[index]=newNode(hash,key,value,null)
3.2发生了哈希冲突,即table[index]!=null(下述步骤也就是查找有没有key相同的节点,存在则返回该节点)
3.2.1根据hash和key,判断插入节点是不是table[index],是则e=table[index]
3.2.1如果插入节点不是table[index]
3.2.1.1如果table[index]是一颗红黑树,红黑树解决哈希冲突,e=putTreeVal(hash)
3.2.1.2如果table[index]是一条链表,链表解决哈希冲突
3.2.1.2.1如果链表中存在key与插入节点key相同的节点,e=node
3.2.1.2.2如果不存在,该节点插到链表尾部,如果链表长度>8,则treeifyBin
(treeifyBin方法可能进行扩容,可能进行链表转换红黑树)
4、table[index]中是否存在key与传入的key相同的节点(e==null?),存在则使用传入的value覆盖oldVal并返回oldVal
5、不存在则判断HashMap中节点数目是否大于threshold,是则进行扩容
==详细过程阅读下段代码:==
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent 如果该参数为ture,那么已存在的key相同的节点旧值不会被覆盖
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//初始化table
if ((tab = table) == null || (n = tab.length) == 0)
//第一次进行put方法时,才创建数组!!!!
n = (tab = resize()).length;
//table[i]为空,没有发生哈希冲突,直接将节点放在table[i]位置
/*
i = (n - 1) & hash,n为table的length
为什么HashMap要求table的长度必须为2的整数次幂?
因为:
在求插入节点放入数组位置的下标时,是用hash与length-1进行按位与运算
如果length不为2的整数次幂,
则length-1如下:
00..00 x..x0x..x
则经过按位与后
00..00 x..x1x..x对应的一些位置永远不会被访问到
既浪费了空间,也增加了哈希冲突的可能性
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//table[i]不为空,发生了哈希冲突
else {
//e代表原table[i]中与传入的hash和key都相等的node
//如果e为null,则返回的oldValue也为null
Node<K,V> e; K k;
//插入的node与table[i]的头结点hash、key都相等
/*
(p.hash==hash)&&((p.key==key)||(key!=null&&key.equals(p.key)))
此处延伸出三个要点:
1、如果HashMap的Key为Object类型,那么需要重写equals方法和hashCode方法
2、向HashMap中put K-V时,尽量用被final修饰的变量作为Key
因为如果不是final修饰,当K-Vput进HashMap并且K改变时,
再进行get(K)就取不到之前的Node,返回的是null
3、java基本类型变量无法作为HashMap的Key,而对应的包装类可以
因为String、Integer、Character等是被final修饰的类,并且重写了hashCode和equals方法
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//table[i]是一颗红黑树,将问题转化为将node插入到红黑树中
//红黑树解决哈希冲突
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//table[i]是一条链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
/*
此处可见,jdk1.8中插入链表时采取的尾插法
*/
p.next = newNode(hash, key, value, null);
//若链表长度大于8,则将链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//返回oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//如果e不为空直接返回了oldValue,HashMap中节点数目不变
//modCount不需要修改
++modCount;
/*
注意:put方法
1.7中是先扩容再添加元素
1.8中是先添加元素再扩容
并且扩容的条件也不同
1.7中要满足1、节点个数达到扩容阈值;2、发生哈希冲突
而1.8中只需要满足条件1就进行扩容
*/
if (++size > threshold)
//扩容
resize();
afterNodeInsertion(evict);
return null;
}
resize方法(重要)
初始化HashMap的table或者对table进行扩容操作。
如果是扩容,则会将原table中的元素移动到新的table中,并且链表中元素的顺序不变
1、计算新的newThreshold、newCapacity,并创建新的table,newTab=new Node[newCapacity]
(2倍扩容,即newCapacity=2*oldCapacity)
2、如果原table不为空,即不是进行初始化,则遍历原tab中的每一个链表或红黑树tab[i]
3、如果tab[i]不为空,tab[i]只有一个元素,则newTab[hash&(newCapacity-1)]=tab[i]
如果tab[i]为红黑树,则该红黑树进行split操作,可能会将红黑树转换成链表
如果tab[i]为链表,则遍历该链表中的节点,创建两个临时链表hiNode、loNode分别用于保存放在 newTab[i+oldCapacity]和newTab[i]中的节点,最后将hiNode、loNode放在newTab中对应位置
==详细过程阅读下段代码:==
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
//初始化或扩容table
final Node<K,V>[] resize() {
/**
* 如果创建hashMap时调用的是无参构造函数
* table = null
* threshold = null
* loadfactor = 0.75
*
* 如果调用的是有参构造函数(initcapacity或initcapacity和loadfactor)
* table = null
* threshold >= initcapacity
* loadfactor != null
*/
Node<K,V>[] oldTab = table;
//初始化table的话oldCap必定=0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//创建hashMap时指定了参数,在构造函数中就为threshold赋予了一个大于等于initCapacity的2次幂
//这个2次幂就是初始化table时table的length
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//创建hashMap时没有指定参数
//该情况下的initCapacity与loadfactor都是采用的默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//创建hashMap时指定初始容量的情况
//或扩容的情况
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;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//方便GC下次回收
oldTab[j] = null;
//如果oldTab[j]只有一个节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果oldTab[j]是一颗红黑树
else if (e instanceof TreeNode)
//对红黑树进行split操作,可能会发生将红黑树转换为链表的操作
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//oldTab[j]是一条链表
/**
* 将table扩容为长度为2*oldTable.length的newTable后
* 原table[j]中的节点,在table扩容后,要么在j,要么在j+oldTable.length
*
* 假设原table长度为 000..000 100..00
* oldTab.length-1==> 000..000 011..11
* hash: xxx..xxx Xxx..xx
* &
*
* 000..000 0xx..xx
* 即为节点在数组中的下标index
* 扩容后table长度为 000..001 000..00
* newTab.length-1==> 000..000 111..11
* hash: xxx..xxx Xxx..xx
* &
*
* 000..000 Xxx..xx
* = 000..000 0xx..xx
* + 000..000 X00..00
* 即为节点在扩容后的数组中的下标newIndex
* index与newIndex的区别在于这个大X:X=1时,newIndex = index+oldTab.length
* X=0时,newIndex = index
* 用000..000 X00..00和oldTab.length进行与操作,就可以判断X是0还是1
*/
else { // preserve order --->由此可知,1.8版本的hashMap扩容后链表元素的顺序不变,而1.7中会发生反序
/**
* jdk1.8的hashMap插入是用的尾插法,1.7中是头插法
*/
//loHead、loTail,lo代表low
//这两个指针代表将要放入到newTab[j]中的链表的头结点和尾节点
Node<K,V> loHead = null, loTail = null;
//hiHead、hiTail,hi代表high
//这两个指针代表将要放入到newTab[j+oldTab.length]中的链表的头结点和尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//此处是用oldCap进行&操作,而不是oldCap-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);
//将链表放到tab的相应位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
treeifyBin方法
将链表转换成红黑树,但并非一定会发生转换,也可能会进行扩容
重点关注判断条件
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果此时数组的长度大于等于64了,才会将链表转换成红黑树,否则对hashMap扩容
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;
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);
}
}
==get、remove、replace方法比较简单,不过多解释,放在此处是方便以后查阅==
get方法
containsKey方法底层也是调用的getNode方法,根据getNode方法的返回值是否为空来返回false/true
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
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) {
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;
}
remove方法
使用该方法时,注意remove方法的返回值
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
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) {
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);
}
}
//前面步骤与getNode方法类似
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
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;
}
replace方法
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
总结
jdk1.7与jdk1.8中HashMap的区别
底层数据结构实现不同:
JDK1.7采用的数组+链表
JDK1.8采用的数组+链表+红黑树
put方法将节点插入链表的策略不同:
JDK1.7头插法
JDK1.8尾插法
put方法中插入节点和扩容的顺序不同:
JDK1.7先扩容再插入节点
JDK1.8先插入节点再扩容
Hash算法不同:
JDK1.7进行了9次扰动处理(4次位运算+5次异或)
JDK1.8进行了2次扰动处理(1次位运算+1次异或)
HashMap解决Hash冲突
该图转载于blog.csdn.net/qq_36520235… ,侵权删。