HashMap这个容器不仅使用的多,同时知识点也很多,特别在jdk1.8引入红黑树,所以在这个容器上记下几笔笔记方便以后查阅。
储存结构
Node节点代码如下:
static class Node implements Entry {
final int hash;//索引
final K key;//键
V value;//值
Node next;//链表下一个Node
Node(int hash, K key, V value, Node 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) {
Entry,?> e = (Entry,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap是数组(称之为哈希桶数组)+链表/红黑树(jdk1.8后)来保存数据的,通过链地址法解决哈希冲突。
主要变量
//默认初始容量:16,必须是 2 的整数次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量: 2^ 30 次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子的大小:0.75,可不是随便的,结合时间和空间效率考虑得到的
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形阈值:JDK1.8新增的,链表超过这个长度时使用红黑树而不再使用链表。必须>2
static final int TREEIFY_THRESHOLD = 8;
//非树形阈值:也是 1.8 新增的,小于该值红黑树变链表,要比 TREEIFY_THRESHOLD 小
static final int UNTREEIFY_THRESHOLD = 6;
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap的值达到这个值时,进行扩容(这个值等于length*loadFactor,但并不是在初始化时就设定的,下文会进行说明)
int threshold;
//实际的扩容印子
final float loadFactor;
HashMap的容量值必须是2的次方数,因为扩容时的hash算法涉及到取模运算 ,
key.hashCode()& (length-1)
倘若length是2的次方,比如二进制数1000(8),0001 0000(16),那么(length-1)是0000 0111(7),0000 1111(15),1的与(&)运算重复值少(因为0的与运算都是0),这样HashMap中元素位置会分布尽可能散一些(少重复)
这个思想在构造函数中调用的tableSizeFor()方法体现出来:
//返回大于输入参数且最近的2的整数次幂的数 比如输入0000 1xxx xxxx xxxx,或者输入二进制0000 0000 0000 1010(十进制10),返回16
static final int tableSizeFor(int cap) {
int n = cap - 1; //0000 01xx xxxx xxxx,0000 0000 0000 1001
n |= n >>> 1; //对n无符号右移1位并进行位或运算:可以看到之前1的地方不变还是1,右移出来的1的位置也是1,也就是0000 011x xxxx xxxx,0000 0000 0000 1001->0000 0000 0000 1101
n |= n >>> 2; //对n无符号右移2为并进行位或运算:同上理得到0000 0111 1xxx xxxx,0000 0000 0000 1101->0000 0000 0000 1111
n |= n >>> 4; //0000 0000 0000 1111->0000 0000 0000 1111
n |= n >>> 8; //0000 0000 0000 1111->0000 0000 0000 1111
n |= n >>> 16; //同理 ,最后可让除最高位都变为1,这样再执行n+1就成了2的次方幂
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//0000 0111 1111 1111最后n+1就是0000 1000 0000 0000,另一个数是(16)0000 0000 0001 0000也就是比10大的最小2的n次幂
}
主要方法
1.确定哈希桶数组索引位置
// 方法一,jdk1.8 & jdk1.7都有:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 方法二,jdk1.7有,jdk1.8没有这个方法,但是实现原理一样的:
static int indexFor(int h, int length) {
return h & (length-1);
}
这里的hash算法本质上是以下3步:(结合上面tableSizeFor方法看)
- 取key的hashCode值,h = key.hashCode();
- 高位参与异或运算,h ^ (h >>> 16);
- 取模运算,h & (length-1)。
最后结果是0101=5,这也和前面数组长度必须是2的n次方幂联系起来,可以看到0的与运算都是0,若低位也是0,出现同一数字的概率会变大。
2.putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//table是否为null
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//扩容
if ((p = tab[i = (n - 1) & hash]) == null)//根据键值key计算hash得到插入的数组索引并判断是否为null
tab[i] = newNode(hash, key, value, null);//是的话直接插入
else {//否
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//key是否存在
e = p;//key存在覆盖掉
else if (p instanceof TreeNode)//key不存在,判断是否是treeNode
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//红黑树插入键值对
else {//key不存在,并且是链表,那么遍历准备插入
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;
}
}
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;
}
3.扩容resize()
final Node[] resize() {
// 当前table
Node[] oldTab = table;
// 当前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当前阈值
int oldThr = threshold;
// 新的容量值和阙值
int newCap, newThr = 0;
/*
1. resize()函数在size > threshold时被调用。oldCap大于 0 代表原来的 table 表非空,
oldCap 为原表的大小,oldThr(threshold) 为 oldCap × load_factor
*/
if (oldCap > 0) {// 1:若旧table容量已超过最大容量,更新阈值为Integer.MAX_VALUE(最大整形值),这样以后就不会自动扩容了。
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
}
/*
2. resize()函数在table为空被调用,table创建了但没添加元素。oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
或 HashMap(Map extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0, oldThr 为用户指定的 HashMap的初始容量。
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
/*
3. resize()函数在table为空被调用。oldCap 小于等于 0 且 oldThr 等于0,用户调用 HashMap()构造函数创建的 HashMap,所有值均采用默认值,oldTab(Table)表为空,oldCap为0,oldThr等于0,
旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
*/
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[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//单个节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//红黑树
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order 链表
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进行判断分成两种情况
//最高位==0,这是索引不变的链表。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//最高位==1 (这是索引发生改变的链表)
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;
// rehash 后节点新的位置一定为原来基础上加上 oldCap,
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
下文扩容更多细节来自 美团技术博客-Java 8系列之重新认识HashMap
经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图: