1、final属性
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;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
2、成员变量
transient Node[] table;
transient int size;
int threshold;
final float loadFactor;
3、构造函数
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;
//初始化的容量存在扩容阈值中
//容量必须是2的正整数次方,tableSizeFor得到大于initialCapacity的最小的2的正整数次方
this.threshold = tableSizeFor(initialCapacity);
}
//计算大于cap的最小的2的正整数次方
static final int tableSizeFor(int cap) {
//例如传入cap是13,对应的二进制为: 1101,则n为1100
int n = cap - 1;
//运行结果n为1100|0110=1110
n |= n >>> 1;
//运行结果n为1110|0011=1111
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//假设原来cap的二进制数从高到低第一位为1所在的位为X
//(1)cap就是二的正整数次方即二进制为 00...0100...00的形式,
// cap-1就为 00...0011...11的形式,经过上面左移或运算后任为00...0011...11
//(2)cap不是二的正整数次方,假设二进制为00...0110...10的
// cap-1就为 00...0110...01,经过上面左移或运算后任为00...0011...11
//将n+1得到结果 00...0100...00 即为cap对应的最小的2的正整数数次方
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
为什么HashMap的容量必须是2的整数次方?
这个设计是为了方便节点在数组中索引位置的计算,
通用情况下是取模运算:节点在数组中的位置=节点的hash值%数组长度,但是取模运算的效率比较低,为了提高运算效率将其修改为位运算,即
节点在数组中的位置=节点的hash值&(数组长度-1)
这个公式等价于取模运算的前题是:数组长度是2的正整数次方。所以HashMap的容量必须是2的整数次方。
4、put方法源码解析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//被称为扰动函数,作用是减少hash冲突
static final int hash(Object key) {
int h;
//h=key.hashcoede,通过Object的hashcode()得到的哈希码与key的内容和内存地址等相关
//前面说过,计算索引的公式是 index=hashcode&(数组容量-1)
//数组容量-1 的形式是00...0011...11即高位是M位的0低位是N位的1,M+N=32(即int为32位)
//因此 计算索引的公式 的计算公式只与低N位有关
//为了减少hash冲突 引进扰动函数 f(h)=h^(h >>> 16)
//即的到的结果是: 高16位保持不变,低16位是高16位与原来低16位异或的结果
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//1、hashmap是中的数组是懒加载的,只有当插入第一个元素时才进行初始化
//数组为空通过扩容完成初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2、数组不为空,但hashcode散列到的位置为空,直接创建节点放入到数组中的该位置
//当n为2的正整数次方时,(n - 1) & hash=hash % n
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
//3、发生了hash冲突,数组上的头节点的key与要插入的key相同,返回老的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//4、发生了hash冲突,数组上的头节点的key与要插入的key不同,头节点是树节点的实现
//4.1、遍历红黑树,有相同的key,返回老的节点
//4.2、无相同的value则创建新的节点插入树中,进行插入后的平衡调整,返回null
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeval(this, tab, hash, key, value);
else {
//5、遍历链表
for (int binCount = 0; ; ++binCount) {
//5.2、遍历完成,未发现key相同的节点,新建节点插入链表尾部,返回空
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//在插入节点时,当前的节点个数为8(插入后为9),触发逻辑treeifyBin()
//触发逻辑treeifyBin(): 当前数组的长度小于64,会先进行扩容;当大于等于64,才会将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//5.1 发现有相同的key,返回老的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//6、e为与key相同的老节点,
// 如果key相同的老节点存在,表示本次为覆盖操作,将其value进行覆盖,并返回老的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//7、本次的操作为插入,当本次插入数据完成后,键值对的总数量大于扩容时,进行扩容,返回null
//注:size指的是总的键值对的数量,而容量指的是数组的长度,扩容阈值也是通过数组的长度*负载因子得到
//当size==扩容阈值的时候,必定存在index数组上index位置上为空,且达到扩容阈值就表示,可能存在链表很长的情况
//负载因子的意义是表示,数组上平均每个位置挂的节点数量为0.75
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put的逻辑:(1)判断数组是否为空,空就通过扩容来初始化数组
(2)通过index=hashcode&(数组长度-1)计算出节点在数组上的索引位置
(3)做数据插入
(3.1)如果数组在index上为空,直接将节点插入到数组的index位置
(3.2)数组上不为空,即存在节点,比较原头节点的key是否与新插入的进行 比较,如果相同则将其覆盖
(3.3)如果不相同,通过节点的对象类型,判断此头节点下接的是链表还是
红黑树。
(3.4)如果是红黑树,做遍历,找到key相同的节点就做value的覆盖,未找 到则创建新节点,将其插入红黑树,并对红黑树做平衡调整
(3.5)如果是链表,做遍历,找到key相同的节点就做value的覆盖,未找到
则新创建节点,插入链表尾部,如果此时链表长度大于8,则运行
treeifybin(数组长度小于64做扩容,否则将链表转为红黑树)
(4)判断当前的节点数量是否大于扩容阈值,大于就做扩容
(5)返回运行结果,是新插入返回空,是覆盖则返回原来节点的value.
5、resize扩容方法源码
final Node[] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //1、原来的数组长度不为0,即非第一次put数据 // (插入数据后数据量大于阈值的情况,链表长度大于8要转化为红黑树但是数组长度小于64的情况) if (oldCap > 0) { //1.1、数组长度已经大于等于最大的数组长度,不再扩容, // 将阈值设为最大的int值,防止再触发扩容 //返回原来的数组 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //1.2、数组长度的长度扩大为原来的两倍 //如果扩大的数组长度后小于最大的数组长度且大于默认的数组长度(16),将新的扩容阈值扩大为原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //2、数组为空,即第一次put数据时触发 //2.1、 有通过构造函数传入的初始大小,该初始大小存在threshold中,扩容阈值=构造参数传入值对应的最小的2的正整数次方*默认的扩容因子 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //2.2、 如果没有传入初始的数组的长度,则使用默认的数组的长度16,扩容阈值=默认的数组长度*默认的扩容因子=16*0.75=12 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; //3、创建新的数组长度等于上面步骤中得到的新的长度 @SuppressWarnings({"rawtypes","unchecked"}) Node [] newTab = (Node [])new Node[newCap]; table = newTab; //4、是正真的扩容而不是第一次插入数据,遍历老的数组将所有的节点重新hash映射到新的数组上 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; //4.1、如果数组上的头节点不为空 if ((e = oldTab[j]) != null) { //4.1.1、 将数组上的引用置空,以便GC回收 oldTab[j] = null; //4.1.1、 数组在该位置上的数据只有一个头节点,直接重新将该节点重新映射到新的而数组上 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //4.1.2、 数组在该位置上头节点下接着一颗红黑树,调用split函数 //split函数逻辑: //将所有的节点根据新的数组做hash映射,由于新数组的长度为原来的两倍且数组长度始终为2的正整数次, // 运行 e.hash%newCap=e.hash&(newCap-1)=e.hash&(oldCap*2-1) // 设oldCap中1所在的位置为X(也等于newCap-1中1所在的最高位) //同一棵红黑树上的所有节点在老的数组中hash映射的结果值是相同的, // 而在新的数组上映射只会有两个结果,结果取决于e.hash在X位置上的值是0还是1 //(1.1)因此可以将原来的一棵树转化为两个链表,loHead链表(X位置为0)和hiHead链表(X位置为0) //(1.2)将loHead链表放在老的位置上,而hiHead链表放在新的位置(即oldCap+老的index上) //(1.3)将长度大于8的链表转化为红黑树 else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); else { // preserve order //4.1.3 数组在该位置上头节点下接着链表,逻辑于split函数类似 //(1.1)因此可以将原来的一个链表转化为两个链表,loHead链表(X位置为0)和hiHead链表(X位置为0) //(1.2)将loHead链表放在老的位置上,而hiHead链表放在新的位置(即oldCap+老的index上) Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; 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); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
情况一(第一次调用put方法触发扩容)即原来的数组为空:
调用构造函数进行初始化时,将初始的容量通过计算对应最小的2的整数次方传入扩容阈值中
(1)获取扩容阈值,如果为0,表示构造函数没有设置数组的初始容量,则容量使用默认的16,
扩容阈值设置为12=默认容量*默认的阈值=16*0.75
(2)扩容阈值不为0,则将容量设置为扩容阈值,设置扩容阈值,新的扩容阈值=扩容阈值*负载因子
(3) 创建对应容量的数组
情况二(正真的触发扩容)即原来的数组是存在:
(1)原来的容量已经大于等于允许的最大容量,直接将扩容阈值设置为int的最大值,然后直接返回
(2)将数组的长度(容量)和扩容阈值设置为原来的2倍
(3)遍历老的数组上的所有节点,重新放到新的数组上
(3.1)老的数组在index上为空直接跳过
(3.2)老的数组在index上只有一个节点,通过 hashcode&(新的长度-1) 定位到新的数组上
(3.2)老的数组在index上接的是链表,通过 hashcode&老的长度,将原链表上的所有节点 分到两个新的链表上即loHead为首的链表(运行结果为0)和hiHead为首的链表链表(不为0)
loHead放在原来的index下,而hiHead放在(index+老的数组长度)下。
(3.3)老的数组在index上接的是红黑树,与链表逻辑相同,遍历后将节点,分到两个链表 上,然后分别凡在index和(index+老数组长度)下,最后如果新的链表长度大于8就将其转 为红黑树
6、触发扩容逻辑的情况
(1)第一次put
(2) 节点的数量大于扩容阈值
(3) 要将链表转化为红黑树,但此时的容量小于64
7、HashMap中的位运算
(1)数组的长度必须时2的N次方
(2)将容量转化为对应的最小的2的N次方,通过 初始值-1的5个左移与运算
(3)hash()扰动函数,通过左移16位的异或运算保持高位不变,低位为高位和低位的异或的结 果,hashcode^(hashcode>>16)
(4)节点在数组中的定位运算,hashcode&(数组长度-1)
(5)扩容时将链表(红黑树)一分为2的运算,(hashcode&oldCap)
8、调试代码
package collections;
import java.util.HashMap;
public class HashMapDebug {
public static void main(String[] args) {
HashMap map=new HashMap<>();
map.put(16,1);
map.put(32,1);
map.put(48,1);
map.put(64,1);
map.put(80,1);
map.put(96,1);
map.put(112,1);
map.put(128,1);
//在index=0位置上的链表插入这条数据后后长度大于8,但此时数组长度为16(小于64)
//会触发扩容逻辑
map.put(144,1);
map.put(160,1);
map.put(176,1);
map.put(192,1);
}
}
public class HashMapDebug2 {
public static void main(String[] args) {
HashMap map=new HashMap<>();
//在index=0位置上的链表插入这条数据8条数据
map.put(16,1);
map.put(32,1);
map.put(48,1);
map.put(64,1);
map.put(80,1);
map.put(96,1);
map.put(112,1);
map.put(128,1);
//在index=1插入5条数据
map.put(17,1);
map.put(33,1);
map.put(49,1);
map.put(65,1);
//插入之后size大于扩容阈值触发扩容
map.put(81,1);
}
}



