文章目录
前言
本文基于JDK1.8分析了HashMap相关知识、寻址算法、数组长度是2的次幂以及相关源码流程。
提示:以下是本篇文章正文内容,下面案例可供参考
一、HashMap 简介
- 可以存储 null 的 key 和 value,但键为 null 只能有一个,值为 null 可以多个,且线程不安全
- JDK1.8之前底层是数组+链表组成,采用拉链法解决哈希冲突,链表采用头插法,多线程可能导致死循环
- JDK1.8底层采用数组+链表+红黑树,当链表长度大于阈值(默认为8)时且数组长度达到64时,将链表转化为红黑树,以减少搜索时间;若数组长度小于64则扩容数组。当扩容红黑树拆分的节点数小于等于6时,则退化成链表
- 默认的初始化大小为16,之后每次扩容是之前的2倍,且使用 2 的幂次方作为哈希表的大小
- 存储时若 key 相同则覆盖原值
数据结构如图:蓝色为数组,红色为链表,红黑为红黑树
二、HashMap 的寻址算法
- 计算对象的 hashCode()
- 再进行调用 hash() 方法进行二次哈希,hashCode值右移 16位异或运算,让哈希分布更均匀
- 最后 (capacity -1) & hash 得到索引
三、HashMap 的数组长度是2的次幂
- 计算索引效率更高:如果是 2的n次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高:hash & oldCap == 0 的元素留在原来位置,否则新位置 = 旧位置 + oldCap
tableSizeFor 方法保证了 HashMap 总是使用2的幂作为哈希表的大小
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
四、HashMap 源码分析
1.成员变量
- loadFactor 负载因子:太大导致查找元素效率低,太小导致数组的利用率低、存放的数据分散,默认 0.75 是一个比较好的临界值。默认容量是 16,当数量超过 16*0.75 = 12 时进行扩容,扩容涉及 rehash、复制数据等操作
- threshold 阈值:等于 capacity*loadFactory,当 size 大于阈值时,需要扩容了
/**
* 默认的初始容量 16.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的负载因子 0.75.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当链表的节点数大于等于 8 时会转化为红黑树.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当链表的节点数小于等于 6 时会退回链表.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 转化为红黑树时数组的最小容量为 64.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 存储元素的数组,总是 2的幂次倍
*/
transient Node<K,V>[] table;
/**
* 存放具体元素.
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 存放元素的个数.
*/
transient int size;
/**
* 每次扩容和更改map结构的计数器.
*/
transient int modCount;
/**
* 阈值(容量*负载因子),当实际大小超过阈值进行扩容
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
/**
* Node的节点类
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 略。。。
}
2.put 添加元素方法具体流程
- put 方法调用的是 putVal() 方法
- 判断键值对数组是否为空,是则进行 resize() 扩容
- 根据 key 计算 hash 得到数组的索引
- 判断 table[i] 是否为 null,成立则新建节点添加元素
- 不成立则先判断 table[i] 是否和 key 一样,相同则直接覆盖
- 否则判断 table[i] 是否为 treeNode,如果是红黑树则在树中插入数据
- 否则遍历链表,尾部插入数据,如果链表长度大于 8,转化为红黑树,插入数据
- 插入后判断实际数量 size 是否超过了阈值,是则进行扩容
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 具体的添加值方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或长度为 0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素放在哪个桶中,桶为空新生节点放入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素,处理 hash 冲突
else {
Node<K,V> e; K k;
// 判断第一个节点 table[i] 的key是否与插入的key一样,若相同则直接覆盖旧值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否为红黑树节点
else if (p instanceof TreeNode)
// 是则放入树种
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是红黑树节点,说明是链表
else {
// jdk1.8采用尾插发
for (int binCount = 0; ; ++binCount) {
// 链表尾部
if ((e = p.next) == null) {
// 在尾部插入新节点
p.next = newNode(hash, key, value, null);
// 节点数量达到阈值,执行 treeifyBin 方法,根据数组决定是否转红黑树
// 只有当数组大于等于 64,才转红黑树,否则执行数组扩容
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判断链表中的节点 key与插入的元素 key是否相等
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 扩容方法具体流程
- 添加元素或初始化时扩容,第一次添加数据数组长度为16,以后每次扩容都是达到阈值
- 每次扩容都是之前容量的 2倍
- 扩容之后,会创建一个新的数组,把老数组的数据迁移到新数组中
- 没有 hash 冲突的节点,直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断 e.hash & oldCap 是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小的这个位置上
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) {
// 如果超过最大值就不再扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则扩容为原来的 2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
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) {
// 创建时指定初始化容量和负载因子,在这里阈值初始化
// 或者扩容钱的旧容量小于 16,在这里计算新的 resize 上限
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];
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)
// 只有一个节点,直接计算元素新的位置即可
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 将红黑树拆分成 2颗树,拆分后节点小于 6则树转化为链表
((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) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
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;
}
4.get 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个节点
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中 get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
总结
HashMap 是线程不安全的类,若要使用线程安全的,推荐 CurrentHashMap,后续有时间会分享该源码。
别人能够替你开车,但不能替你走路;能够替你做事,但不能替你感受。人生的路要靠自己去走,成功要靠自己去争取。天助自助者,成功者自救。