本篇不介绍红黑树,后续会单独分析。
- JDK 1.8
也没看过jdk1.7 所以不做对比分析了
需准备知识
- 异或运算
- 位移
- 知道HashMap的数据结构有哪些
- 知道单链表大致是怎么一回事
能学到什么
- HashMap如何计算扩容阙值
- 哈希表需要树化的标准条件
- 哈希表什么时候初始化
- 为什么需要扩容
- HashMap如何增加hash的散列性
- HashMap如何确定桶位
- 链表如何删除元素
这些问题都会在源码分析的注释中有体现出来。
可能有些没分析到,欢迎留言,后续继续补充。
文章目录
- HashMap核心属性
- HashMap构造方法
- 哈希表元素:Node
- 如何计算扩容阙值:
tableSizeFor
方法 - 如何加hash的散列性:
扰动函数hash()
- 如何路由寻址(计算出桶位):`桶位 = (table.length-1)& hash1
- HashMap核心方法:
put
、resize
、get
、remove
、replace
- 本篇不介绍树,会额外分析
HashMap 核心属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
/**
* table默认大小的阙值
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* map容量的最大阙值
*/
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;
/**
* 哈希表的所有元素个数超过64,才允许树化
* 树化的标准条件:1. 链表长度>= 8
* 2. 哈希表元素个数超过64个才允许树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 哈希表
* 问题:什么时候初始化?
*/
transient Node<K,V>[] table;
/**
* 哈希表元素个数
*/
transient int size;
/**
* 当前哈希表结构修改次数。(替换不会变)
*/
transient int modCount;
/**
* 扩容阙值,哈希表中的元素超过阙值,触发扩容
*/
int threshold;
/**
* 负载因子,可设置
* threshold =capacity * loadFactor
*/
final float loadFactor;
}
HashMap 构造方法
/**
* 第一个参数:initialCapacity --> 使用者设置的哈希表的大小
* 第二个参数:loadFactor --> 设置的加载因子
*/
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;
// 通过tableSizeFor计算出扩容阙值
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
哈希表元素:Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // key经过扰动函数hash计算得出的hash值
final K key; // 要存储的key
V value; // 要存储的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;
}
如何计算扩容阙值: tableSizeFor
方法
/**
* Returns a power of two size for the given target capacity.
* 返回当前>=cap的一个数字,并且是2的次方数
*
* 假如输入cap的是27
* n = cap - 1 =26
* 26的二进制是 11010 => 即n的二进制是11010
* 运算规则:
* n |= n >>> 1; 的意思是:n 按位"或运算" | n向右位移1位的结果,得出的结果,并同时赋值给n
* 11010 |= 01101 => 11111 ( 01101 是n向右位移1位的结果,然后与n进行"或运算" 得出11111,并赋值给n。以下相同规则)
* 11111 |= 00111 => 11111
* 11111 |= 00001 => 11111
* 11111 |= 00000 => 11111
* ..... 后面的位移都一样了
*
* 最后n的二进制111111
* 二进制111111 = 31
*
* 返回结果:
* return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
* 解释--> n=31 执行 (n >= MAXIMUM_CAPACITY)
* (n=31)<MAXIMUM_CAPACITY 执行 n + 1
* 最后返回结果:32
*/
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;
}
如何增加hash的散列性:扰动函数hash()
/**
* 作用:让key的hash值高16位,参与路由运算。减少hash值的碰撞
*
* 1. key=null 插入第一位
* 2. (h = key的hashCode) ^ (h右移16位)
* 异或算法:相同返回0 不同返回1
* 例如:
* h = 1001 0011 1011 1111 0000 0110 1000 0011
* h >>> 16 =>0000 0000 0000 0000 1001 0011 1011 1111
*
* 1001 0011 1011 1111 0000 0110 1000 0011
* ^
* 0000 0000 0000 0000 1001 0011 1011 1111
* =
* 1001 0011 1011 1111 1001 0101 0011 1100
*
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如何路由寻址(计算出桶位)
桶位 = (table.length-1)& hash
HashMap核心方法 put
、 resize
、get
、remove
、replace
put -> putVal
源码
先看一下put数据时的大致流程图:
源码注释比较多,后面对putVal源码的执行顺序做了文字总结
、流程图总结
。结合起来看更容易理解:
先贴出源码流程图小结,方便对比源码查看流程
putVal源码流程图小结
对应源码中代码的执行顺序,可对比着源码查看:
putVal源码:
/**
* 问题: HashMap什么时候初始化哈希表的?
* 答:第一次调用put时初始化。
* 问题:为什么第一次put才去初始化哈希表?
* 答:为什么第一次put才会初始化因为哈希表初始化会占用很大内存,用户可能只是new HashMap而没有使用
*
* 路由寻址作用:通过路由寻址确定要插入的元素,在table中应该插入的桶位
* 路由寻址公式 : index下标 = (table-1) & hash
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K, V>[] tab; // 临时table
Node<K, V> p; // 临时存储的元素对象(解释:通过路由寻址,找到的元素)
int n, i; // n:是临时的table长度 i:通过路由寻址赋值的table的桶位(即:table表中的下标)
// 分析1 --> table不存在初始化哈希表
// 解释 --> table==null 或 table长度为0。哈希表不存在,触发扩容机制。 同时完成tab、n 的赋值
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* put数据时的2种情况:
* 路由寻址算法:桶位 = (table-1) & hash
* 1.该桶位不存在数据 if
* 2.该桶位已经存在数据 else
* */
// 分析2 --> 这个桶位不存在元素直接存入该桶位
// 解释 --> 通过路由寻址 确定index, 检测index在table中元素是否存在。不存在直接存入该桶位。同时完成 p 的赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 分析3 --> p 元素存在值,则属于 链表、红黑树 情况
// 解释 --> p 在上面if判断中已经赋值。(即:当前该桶位已经存在的元素)
Node<K, V> e; // 临时的元素对象
K k; // 临时key
// 分析3.1 --> table 中已经存在元素和要存储的元素重复
/**
* p.hash:当前元素的key的hash值
* 解释 --> 条件1: p.hash == hash : 当前table[index]元素对象的key的hash值和要存储元素的key的hash值相同
* 解释 --> 条件2: ((k = p.key) == key || (key != null && key.equals(k)))): 当前元素的key和要存入元素的key相等 或 (要存入元素的key!=null 且 要存入元素的key和当前桶位的元素的key相同)
* 满足2个条件,说明要存入的元素和当前桶位的元素相同
* 对临时元素对象e赋值
* 后面对e进行判断,e!=null 的情况,会进行覆盖
* */
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//分析3.2 --> 检测p元素是否在树中
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 分析 3.3 --> 链表。 说明链表头元素和和当前要存入的元素不同
// 解释 --> 遍历链表
for (int binCount = 0; ; ++binCount) {
// 分析 3.3.1 --> 遍历整个链表 p.next==null 没找到与当前要插入的的key的node
// 解释 --> 形成链表的关键,就是靠Node元素内的next来进行指向下一个元素的
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//分析 3.3.2 --> 判断插入当前元素后,检查是否达到树化标准:1.链表>8 2.table 元素个数超过64
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);// 树化
break;
}
// 3.3.3 说明在链表中找到了,与当前要插入的key相同的node元素。break结束循环,后续要替换操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 3.4 e != null 新值替换旧值 并返回老值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 4. 记录对table操作的次数
++modCount;
// 5. 再次检测是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}w
putVal源码流程文字小结
-
map第一次put时,哈希表不存在元素,会触发扩容机制
-
通过路由寻址确定要插入元素的桶位,当前桶位无元素,则直接插入该桶位
-
前2个条件不符合,说明该桶位已经存在元素
3.1 通过比对要插入的Node和当前桶位Node的key的hash值 && key是否相同,相同则对临时Node赋值,后续会进行新值替换
3.2 已经树化
3.3 遍历链表
3.3.1 遍历查找与当前要插入的Node是否有相同,(e=p.next)=null 说明没有存在的,插入链表 3.3.2 插入后,检测是否满足树化标准条件,满足的话进行树化。 3.3.3 同3.1的比对逻辑一样。 是相同元素则break 结束循环
3.4 e != null 新值替换旧值 并返回老值
-
记录对table操作的次数
-
再次检测是否需要扩容:符合条件进行扩容
resize
扩容机制
扩容核心做了2件事情:
resize源码:
源码中注释已经写很详细。需要看官,脑海中有个结构。这段源码后面会放出美团技术团队博文中的一张图:
/**
* 问题:为什么需要扩容?
* 答:为了解决hash冲突导致的链化影响查询效率的问题,扩容会缓解该问题
*/
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table; // 临时变量:扩容前table
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 临时 扩容前table长度
int oldThr = threshold; // 临时变量:触发扩容前阙值
int newCap, newThr = 0; //newCap = 临时变量:扩容后table长度 newThr = 临时变量:扩容之后,下次触发扩容阙值 后面newThr会赋值给threshold
/* ----------------- 计算 newCap newThr -----------------*/
// 分析 1 --> 计算新的newCap值(临时新table长度) 、newThr值(临时新扩容阙值)
// 分析 1.1 --> 哈希表已经初始化过
if (oldCap > 0) {
// 分析 1.1.1 --> 哈希表长度大于定义最大范围(这种情况一般不会发生)
// 解释 --> 向左位移1位即:扩大一倍。所以 1 << 30是一个很大的数字
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 分析 1.1.2 --> 向左位移1位(扩大一倍)
// 解释 --> 满足2个条件:1.当前哈希表的大小 < 定义的最大值 2. 当前哈希表的大小 > 哈希表的最小初始值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 位移后的扩容阙值,赋值给newThr
}
// 分析 1.2 --> 通过HashMap其他三个构造方法初始化.
//解释 --> 只有new HashMap() 没有对threshold进行初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// 分析 1.3 --> // 即:oldCap=0 , 当前table=null
// 解释 --> 说明当前是通过new HashMap() 初始化的。
// newCap : table
// newThr 扩容阙值: 默认大小 * 负载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 分析 --> 1.4 对上述计算newThr进行检测。如果条件成立 需要重新计算出newThr
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
/*----------------- 扩容开始 -----------------*/
//分析 2--> 真正开始扩容
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;//扩容之后的哈希表 赋值给 table
if (oldTab != null) {// 确定原来的哈希表不为null才进行扩容
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) { // 当前桶位存在元素
oldTab[j] = null; // 把当前桶位置为null,方便回收。上面if判断已经赋值给e
//2.1. e.next == null 说明当前桶位只有一个数据(不是链表、树)
// 把当前元素,通过路由寻址找到在新哈希表中的桶位,直接存入该桶位
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//2.2. 该节点,已经树化
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
//3.3. 链表
else { // preserve order
//底位链表:存放在扩容之后的数组的下标的位置,与当前数组下标的位置一致(例如:table 16扩容到32 ,假如15下标位置的数据,扩容后数据还是在15这个位置)
Node<K, V> loHead = null, loTail = null;
//高位链表:存放在扩容之后的数组的下标的位置 --> 当前数组下标位置+扩容之前数组的长度
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
//对链表进行拆分,分别存入低位链表、高位链表
/**
* 例:
* e.hash存在2种情况: 高位要么是1要么是0
* 1. 1 1111
* 2. 0 1111
* 假设oldCap=16 =>二进制: 1 0000
*
* &运算:
* 1 1111
* & 1 0000
* = 1 0000 =>16 说明高位是1 存入高链
*
* 0 1111
* & 1 0000
* = 0 0000 =>结果为0 说明高位是0 存入低链
* */
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; // 目的是把链断开 存入低链时,next还指向一个高位链的数据,所以需要断开
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;// 目的是把链断开
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
美团博文中的图:
get -> getNode
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab; // 临时table
Node<K, V> first, e; // first -> 头元素
int n; // table长度
K k;
/**
* 两个条件:
* 1. 哈希表不能为null
* 2. 路由寻址,桶位不能为null
* */
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
/**
* 第一种情况:
* 检查当前桶位元素
* 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同
* 2. 当前桶位的Node的key和要取的key相同
* 满足2条件则返回当前桶位的Node
* 进入此判断:
* 说明当前桶位只有一个元素
*/
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
/**
* 第二种情况:
* 当前桶位的Node的next不为null,则当前桶位是 链表 或 树
* */
if ((e = first.next) != null) {
// 树查找
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
// 链表查找
do {
/**
* 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同
* 2. 当前桶位的Node的key和要取的key相同
* */
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove -> removeNode
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;
// p -> 根据寻址结果得到的桶为元素Note
// n -> 哈希表长度
// index -> 寻址结果
/*---------分析 1 --> 查找要删除的Node ---------*/
/**
* 两个条件:
* 1. 哈希表不能为null
* 2. 路由寻址,桶位不能为null
* */
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
//
//e --> 当前桶位元素的下一个元素
K k;
V v;
/**
* 分析1.1 --> 同getNode的比对逻辑是一样的
* 赋值给临时node
* */
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; //
/**
* 分析1.2 -->
* 当前桶位的Node的next不为null,则当前桶位是 链表 或 树
* */
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)))) {
// 链表中到元素 break结束循环
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
/*---------分析 2 --> 删除找到的Node ---------*/
/**
* Node不为null,说明找到了要删除的Node
* */
// Node不为null 比对要删除的value是否相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 分析 2.1 --> 树删除
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
//分析 2.2 --> 把node的下一个元素,指向当前桶位
// 解释 --> 只有一种情况,就是分析1.1 -->这种情况,当前的桶位元素即为要删除的情况
else if (node == p) //
tab[index] = node.next;
//分析 2.3 --> (链表中间元素删除)将当前桶位要删除元素的上一个元素next指向node的下一个元素
/**
* 解释-->:1,2,3三个元素
* 关系: 1.next指向2
* 2.next指向3
* => 2是中间元素
* 删除:2是要删除的元素:
* => 把1的next指向2的next(2的next是3) 即: 1.next = 3
* 这样就完成链表中间元素的删除
*
* */
else
p.next = node.next;
// 同步哈希表的操作次数
++modCount;
// 同步哈希表的长度
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
replace
getNode获取元素
@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;
}