1.HashMap采用的数据结构
Node数组 + 链表/红黑树
Node节点的定义
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;
}
链表解决Hash冲突
//HashMap的putVal部分源码(当数组节点有值并出现Hash冲突时,判断节点类型不是红黑树节点时,
//会以链表形式追加元素)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
Node数组作为桶数组存放元素,采用链地址法解决Hash冲突(而ThreadLocal采用线性探测再散列解决Hash冲突),当数组元素大于64,链表长度大于8则将链表转换为红黑树(因为链表元素查询的时间查询复杂度为O(n),元素较少时查询效率可以接受,但元素足够多时,查询效率过低;转为红黑树后查询时间复杂度为O(logN),查询效率比链表更优)
2.选用红黑树而不选用B+或者AVL平衡树原因:
①对于B+树而言,是MySQL中的索引结构,叫做多路平衡查找树。相比红黑路径更多,每个节点存放多个索引key,结构矮胖,因此适用于数据量大的场景,矮胖使得MySQL的IO次数更少,大数据量查询效率远超红黑树;当数据量较少时会退化成线性结构,并且作为多路平衡查找树,每个节点还会存放下一层相邻节点的指针,空间消耗大。红黑为二叉平衡查找树,数据量不是足够大的情况下,空间占用方面更优。
②而对于AVL平衡树而言,它是严格的平衡树,时间查询复杂度为O(logN),最短路径和最长路径插值不能超过1;因此针对于HashMap这种频繁的插入删除就不适合,会频繁进行树结构的调整(树旋转保持平衡状态),严重影响效率。而红黑树是非严格平衡二叉树,根到叶子的最长路径不超过到最短路径的两倍,就不会进行平衡调节,因此针对数据频繁插入删除的场景,红黑树保留和AVL一样的查询时间复杂度(OlogN),并且不会频繁调整树结构而损失效率。
3.容量永远是2的幂
①当new HashMap()时不传入指定容量时,默认容量为16;以后每次扩容都采用位运算变为原来容量的2倍
默认容量:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
resize()方法扩容为2倍部分源码:
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //判断旧容量oldCap是否大于最大容量2的30次方
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //newCap新容量为旧容量左移1位
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
}
②当传入指定初始化容量时,调用tableSizeFor(int cap)方法得到2的幂的容量
//通过4次或运算,保证了最高位为1,后面所有位都为0,容量必为2的指数
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;
}
容量永远是2的幂原因:
个人观点:当HashMap中put一个元素时,会将Hash取模映射到一个数组下标。源码中采用 hash & (n - 1)得到数组下标,位运算直接基于底层二进制进行运算,效率比 hash % n 更高。采用hash & (n - 1)有一个局限,就是hash冲突的问题。
putVal()方法中的部分源码:
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //(n - 1) & hash得到数组下标
tab[i] = newNode(hash, key, value, null);
举个例子:当前hash为26,n为16(2的幂)时
hash & (n - 1)等价于:
0001 1010
0000 1111
------------------
0000 1010 (十进制为10)得到数组下标index与多个bit位相关
当n为17(非2的幂时)
0001 1010
0001 0000
----------------
0001 0000 (十进制为16)得到数组下标index只由n - 1的最高bit位决定(简言之只由1个bit位决定)
综合:当容量为2的幂时,n - 1保证最高位之后的所有位都为1,与hash做&运算时得到的index下标由多个bit位决定,因此不容易产生hash冲突;当非2的幂,index只由n - 1的最高bit位决定,会频繁产生hash冲突
4.HashMap的put元素过程
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()初始化Node数组
if ((p = tab[i = (n - 1) & hash]) == null) //通过hash取模判断该下标元素是否null
tab[i] = newNode(hash, key, value, null); //元素为空直接newNode放入元素
else { //元素非空情况下
Node<K,V> e; K k;
if (p.hash == hash && //判断首节点元素的key是否相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //相同直接覆盖该节点
else if (p instanceof TreeNode) //若节点为红黑树系节点,用putTreeVal放入元素
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { //链表情况下遍历链表,尾部插入元素
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //当链表长度大于8触发红黑树转换
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //判断是否存在同样的key,存在直接覆盖
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key 当key出现覆盖情况,返回旧值value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //记录扩容次数
if (++size > threshold) //判断元素总数是否大于threshold,满足则触发resize()扩容
resize();
afterNodeInsertion(evict);
return null;
}
5.HashMap的resize()扩容过程(当HashMap的元素个数>=Threshold * loadFactor触发扩容)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //得到旧数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取旧数组长度
int oldThr = threshold; //旧数组扩容阈值
int newCap, newThr = 0;
if (oldCap > 0) { //旧数组长度大于0,判断是否大于Integer.MAX_VALUE(2的30次方)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab; //旧数组长度大于最大长度,不触发扩容,直接返回oldTab
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //获取newCap(oldCap的两倍),oldCap得大于等于初始化默认容量16
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;
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]; //初始化新数组newTab
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { //遍历旧数组每个元素
Node<K,V> e;
if ((e = oldTab[j]) != null) { //当该下标元素不为空时
oldTab[j] = null;
if (e.next == null) //非链表状态,hash取模放入newTab
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //节点为红黑树节点,split方法分割放入
((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;
if ((e.hash & oldCap) == 0) { //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) { //低位链表,对应newCap的下标不会发生变化
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) { //高位链表,对应newCap的下标 + oldCap
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; //返回链表
}