说明:本次源码分析只分析到链表没树化之前
1.8
数组+链表+红黑树
哈希表:数组,数组下放存放链表or红黑树
Node<K,V>[] table;
成员变量: hash记录hashCode ,key记录键,value ,next 下一个节点
final int hash;
final K key;
V value;
Node<K,V> next;
1.初始容量必须为2的n次方(原因:方便进行&运算、方便扩容之后数据移动),后面会说明的
默认为16
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2.new 不会创建数组,第一次put的时候才创建,hashmap初始化并没有初始化table
3.常量:
默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
链表转化成红黑树的阈值,当链表节点大于等于8,并且表长度大于等于64才会树化
static final int TREEIFY_THRESHOLD = 8;
结合上一个阈值,判断树化条件之二
static final int MIN_TREEIFY_CAPACITY = 64;
红黑树退化成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容
下一次扩容的阈值
The next size value at which to resize (capacity * load factor).
int threshold;
加载因子
final float loadFactor;
HashMap被修改的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作)需要抛出异常ConcurrentModificationException
transient int modCount;
key的个数
transient int size;
分析hashMap的成员变量过程中, threshold并没有默认值
如果使用带参数(默认容量的情况下),阈值会被初始化成第一个大于
等于cap 2的n次幂,第一次插入,原表为null,需要扩容,这样就能保证容量一定是2的n次幂
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;
}
4.put操作
第一步 先计算hash值 高位向低位移动16位
为了能让 HashMap 存取⾼效,尽量少碰撞,也就是要尽量把数据分配均匀。
获取Object默认的hashcode,将达hashcode跟向右移动16位的hashcode进行异或运算。为什么?答:异或运算的结果大小值比较均匀分散
一般称为扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
第二步:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
然后:
数组为空,则初始化调用resize()函数
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
然后插入有多种情况
情况1:表的位置为空
数组容量跟hash值&运算,如果这个位置还是空的,直接放入数据
其实就%运算
(容量必须2的n次方,因为只有这样才能保证用&运算可以代替%)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
情况2:哈希冲突
2.1: 哈希值相等、key也相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
也就是刚好头节的key完全跟这个key一致,判断是先判断hash值,
hash值一样,再判断key是否相等,也就是说hash值一样,也不能保证
key一定相等。这也就是重写equals方法的同时也一定要重写hashcode方法(扩展)
改变value值即可
2.2:判断是不是树形结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
红黑树添加操作
2.3:哈希冲突,添加或者替换
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 长度等于阈值就去尝试树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
treeifyBin(tab, hash);这个操作链表树化(不一定,前面说过树化又两个判断条件)
那它是如何进行判断的呢?点进这个方法看看(下方代码块)
这里第一个if判断,判断当前哈希表长度(默认长度大于等于64树化)是否满足树化要求
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;
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);
}
}
5.真正的扩容
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取原来哈希表的长度
int oldThr = threshold;//原来的加载因子(默认0.75)*当前容量
int newCap, newThr = 0;
if (oldCap > 0) { //如果不是新建的
if (oldCap >= MAXIMUM_CAPACITY) { //判断是否大于最大的容量 1<<30
threshold = Integer.MAX_VALUE; //一般都是不大于的
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //判断扩容是否
oldCap >= DEFAULT_INITIAL_CAPACITY) //小于最大容量
newThr = oldThr << 1; // double threshold //原来的容量大于
} //默认的容量
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) {//把哈希表的值赋值给e
oldTab[j] = null; //直接赋null给旧哈希表,估计是垃圾回收机制
if (e.next == null) //如果next值为空,说明链表只有一个节点
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //是树结构,红黑树进行操作
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 说明next值还有
Node<K,V> loHead = null, loTail = null; //不变的哈希位置
Node<K,V> hiHead = null, hiTail = null; //改变的hash位置
Node<K,V> next;
do {
next = e.next; //下一个节点
if ((e.hash & oldCap) == 0) { //哈希表长度2的*次方的
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;
}
}
}
}
}
第一次利用2的 * 次方进行&运算
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
假设原哈希某一位置放置了n个元素
例如1,17两个key的哈希值后4位一定是相等
(补充数值的高16位跟低16位进行^之后 还是等于本身)
17: 10001
1: 00001
16的二进制位:10000
进行&运算,只需要看最高一位
最高位等于1 ,位置等于原位置+原哈希表的容量
最高位等于0,位置不变