HashMap底层类继承实现图
首先来看一下HashMap底层类的继承图
HashMap
最高层的接口是Map接口。总的来说,HashMap
继承了抽象AbstractMap
实现类,并且实现了Serializable
接口和Cloneable
接口,这两个接口的具体功能请查看我关于ArrayList实现类的源码解析。
HashMap类的基本特点
- 以Key-Value的格式存储,key值不可以重复,value值可以重复
- 存储的key值和value值可以为null,但null值当做key键存储只能有一个,null值作为是value值可以有多个。
- HashMap是线程不安全实现类。与HashTable有很大区别,HashTable是线程安全实现类。key和value值都不可以重复,且key值不能为null值。
- HashMap类的初始化容量为16,每一次达到扩容后,扩容的容量是原来的两倍。
- java1.8之前的底层数据结构是数组和链表。数组是HashMap的主体,jdk1.8以后的 HashMap 在为了解决哈希冲突而进行了改变,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会优先选择数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
- 使用2的幂次方作为哈希表的大小。因为在jdk7的HashMap类是基于数组和链表实现的,jdk8之后添加了红黑树,在HashMap类中,当某个key-value需要存储到数组里面时,必须要有对应的数组下标index,而且这个数组下标是不可以越界的,在存储过程中,首先由key得到hashCode值,hashCode是一个数值,它通过hashCode&(table.length-1)运算可以得到一个数组下标,这种运算方法是与运算,比取余运算速度快,因为是通过位运算进行的。在这里就可以知道为什么必须要使用2的幂次方作为哈希表的大小了,因为计算数组下标的运算方法限制:位运算
HashMap类底层数据结构解析
类的主要结构代码
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
这里没什么好说的,该说的上面已经提及。
类的主要变量
/**
* 默认的初始容量,必须是2的幂次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/*
* 最大容量,如果一个更高的值由任何一个带参数的构造函数隐式指定时使用。
* 必须是 2 <= 1<<30 的幂。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
// 构造函数中未指定时使用的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树形阈值,该值必须要大于2并且应当至少为8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 取消阈值, 最多 6 以在移除时进行收缩检测。
*/
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形容量,可进行树化的最下容量,
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;
加载因子:
loadFactor 加载因子是控制数组存放数据的疏密程度
,loadFactor
的值的范围是0到1,当loadFactor的值越接近于 1,那么 数组中存放的数据也就越多,存放的密度也就约密,而且链表的长度也会随之增加,loadFactor
的值越小,越接近于0,,数组中存放的数据也就越少,存放的密度也就越稀疏。
如果loadFactor
的值太大,当程序中需要查找元素时会导致查找元素效率低,如果loadFactor的值太小导致数组存放数据的利用率低,这样子存放的数据会很分散。loadFactor
的默认值为 0.75f,这个是官方给出的一个比较好值
HashMap类给定的默认容量为 16
,负载因子为 0.75f
。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12
就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据
等操作,这是非常消耗性能的。
临界值threshold:
threshold = capacity * loadFactor
,当 size>=threshold
的时候,那么就要考虑对数组的扩增了,也就是说,临界值threshold是衡量数组是否需要扩增的一个标准。
Node节点类源码
static class Node<K,V> implements Map.Entry<K,V> {
// 存储hash值
final int hash;
// HashMap的key值
final K key;
// HashMao的value值
V 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;
}
//
public final K getKey() {
return key; }
public final V getValue() {
return value; }
// 重写toString方法
public final String toString() {
return key + "=" + value; }
// 获取hashCode值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写Object类的equals方法
public final boolean equals(Object o) {
// 判断是当前的对象
if (o == this)
return true;
// 判断是否是Map.Entry
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
// 调用Object方法判断key值与value值是否相等
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
树节点类源码解析
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左节点
TreeNode<K,V> right; // 右节点
TreeNode<K,V> prev; // 前一个节点,需要在删除时取消链接
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* 返回包含此节点的树的根。
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
HashMap类的hash源码对比
在JDK1.7和JDK1.7之前的HashMap类的底层是数组和链表,两者结合在一起就是链表散列。HashMap
通过 key 的 hashCode
经过扰动函数处理过后得到 hash 值,也就是调用HashMap类的hash方法,使用这个方法也就是实现扰动,可以防止一些实现比较差的hashCode
方法从而产生哈希碰撞。通过 (table.length - 1) & hash
判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
拉链法:
将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
jdk1.8的HashMap类的hash源码:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
jdk1.7的HashMap类的hash源码:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
代码分析:
整体来说,JDK7的HashMap类的hash方法实现的整体性能差,而JDK8实现的HashMap类的hash方法性能比较好,因为JDK7的HashMap类的hash方法扰动了四次。
HashMap类的构造方法
默认构造方法:
// 构造一个具有初始默认容量的空HashMap对象。
public HashMap() {
// 适用于其他字段
this.loadFactor = DEFAULT_LOAD_FACTOR; defaulted
}
指定容量大小的构造方法:
// 构造一个具有指定初始容量和默认加载因子 (0.75) 的空HashMap,
// initialCapacity为指定的容量大小数值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
指定容量大小和加载因子的构造函数:
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;
this.threshold = tableSizeFor(initialCapacity);
}
包含一个Map的构造函数:
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
putMapEntries()方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 获取table大小
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) {
// 如果没有初始化,重新计算容量负载因子。s为table的大小
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果超出最大的阈值,则重新计算并设置阈值。
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果已经初始化table,而且table的大小已经超出阈值,则扩容
else if (s > threshold)
resize();
// 遍历每一个元素,然后调用putVal()方法存储
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
HashMap类添加元素
HashMap
类添加元素首先需要根据hash
值计算数组下标,如果该位置没有元素,那就直接插入,如果定位到的数组中有元素,那就需要和该元素比较hash
值和key
值,如果key
值相等,那就直接覆盖,如果key
值不相等,那就需要判断该节点是否为树节点或者是链表节点,如果是树节点,那就直接调用树节点的插入元素的方法e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果是链表节点,那就需要遍历链表节点并比较是否是相同的元素,如果实现相同的元素,那就执行值覆盖,返回旧值,如果链表中不存在相同的元素,那就把添加进的元素插入链表的尾部。
注意: 使用者只能使用HashMap
提供的put()
方法,而不能使用putVal()
方法。putVal()
方法只能内部使用。
// 将指定的值与此映射中的指定键相关联。如果映射先前包含键的映射,则替换旧值
public V put(K key, V value) {
// 传入key的hash值,key值,value值
return putVal(hash(key), key, value, false, true);
}
/**
* @param key的hash值
* @param key的值
* @param 要存放的value值
* @param onlyIfAbsent 如果为真,则不改变已存在的值。
* @param evict 如果为 false,则表处于创建模式。
*/
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是否初始化完成,没有初始化完成则调用扩容函数
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);
// 如果桶中已经存在了其他的元素,不为空
else {
Node<K,V> e; K k;
// 分别比较hash值,key值是否相等而且不为空,如果符合,则取出这个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 使用 e来存储 p
e = p;
//如果hash值不相等,也就是key不相等;则判断节点是否为树节点
else if (p instanceof TreeNode)
// 调用红黑树插入值的方法将元素数据存放进树节点中。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 上面的底层数据结构都不是,那就是链表的数据结构了。
else {
// 遍历到链表最后的节点
for (int binCount = 0; ; ++binCount) {
// 为空则是最后的节点。
if ((e = p.next) == null) {
// 在节点尾部插入数据
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只能对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 判断新添加的元素是否存在于链表中,也就是判断hash值和key值是否相等且不为空。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 直接跳出,因为已经存在了该值。
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
if (e != null) {
V oldValue = e.value;
// 进行值覆盖,然后返回旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果是由于新添加的元素导致容量达到最大值,则执行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
JDK8与JDK7的HashMap类的put方法对比
put(K key, V value)
方法是将指定的key, value对添加到HashMap里。首先判断table是否为空,然后会对table
做一次查找,看是否包含该元素,如果已经包含则执行值覆盖,然后直接返回,查找过程类似于getEntry()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)
方法插入新的entry
,插入方式为头插法
。
public V put(K key, V value)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 查找数组的位置
int i = indexFor(hash, table.length);
// 遍历数组存储的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
// 先遍历
Object k;
// 如果值相等,那就直接覆盖,然后返回旧值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果元素不存在,那就执行插入
addEntry(hash, key, value, i); // 再插入
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断负载因子如果超过阈值,且传入的桶数组不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
//自动扩容
resize(2 * table.length);
// 重新计算哈希
hash = (null != key) ? hash(key) : 0;
bucketIndex = hash & (table.length-1);
}
//在冲突链表头部插入新的entry
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
HashMap类获取元素
public V get(Object key) {
Node<K,V> e;
// 查找数组元素,如果查找为空,则返回null,如果不为空,那就返回对应的值。
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;
// 判断table不为空且长度大于0,然后根据hash值计算数组下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 找出数组下标,判断第一个节点是否就是要寻找的节点元素。
if (first.hash == hash && // always check first node
// 检查对应的key值是否相等
((k = first.key) == key || (key != null && key.equals(k))))
// 确定第一个就是要找的,直接返回
return first;
// 第一个不是要找的,判断节点结构
if ((e = first.next) != null) {
// 如果节点是树节点
if (first instanceof TreeNode)
// 返回树节点查找元素的方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 如果是链表节点,那就一个个循环遍历并比较对应的key值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 找到直接返回
return e;
} while ((e = e.next) != null);
}
}
// 元素找不到返回为空
return null;
}
HashMap类删除元素
// 根据key值删除对应的映射
public V remove(Object key) {
Node<K,V> e;
// 删除元素,如果删除的元素找不到,则返回null,如果成功删除值,那就返回要删除对应的值。
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
// 判断table是否不为空且长度大于0,根据hash值计算数组下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 判断第一个是否就是要删除的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 存储要删除的节点
node = p;
// 判断接下来的数组存储的结构
else if ((e = p.next) != null) {
// 如果数组中存储的是树节点
if (p instanceof TreeNode)
// 调用树节点查找元素的方法,使用node节点存储要删除的元素
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 如果数组中存储的是链表
do {
// 遍历并判断每一个节点是否是要删掉的元素,查找对于的key值。
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 判断是否找到要删除的元素,或者是传入的要删除的值是否为空
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果要删除的元素是树节点,那就调用对应的树节点删除元素的方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果要删除的元素是链表且是第一个
else if (node == p)
tab[index] = node.next;
// 删除的元素是链表且不是第一个
else
p.next = node.next;
++modCount;
// 删除长度
--size;
afterNodeRemoval(node);
// 返回删除的节点数据
return node;
}
}
// 删除不成功
return null;
}
HashMap类的扩容流程
开辟内存空间: HashMap的扩容需要数组扩容,但是数组占用的是连续的内存空间,所以一旦需要扩容就需要重新开辟内存空间。
创建2的幂次方大小的数组: 因为HashMap计算数组下标的方法是通过位运算实现的,所以数组的大小必须是2的幂次方。
遍历数组并转移数据: 创建完数组后,就遍历旧数组上的每一个位置,如果数组位置上是一个链表,那么就把这个链表上的元素全部转移到新的数组上。
JDK7和JDK8扩容机制的不同对比
JDK7及其之前的java版本中HashMap的底层是链表加数组的结构,那么扩容的过程中,需要遍历链表上的每一个元素,然后按照每一个元素的hashCode值进行计算出新数组的下标,
JDK8之后的HashMap类的底层数据结构是数组和链表加红黑树,至于什么时候使用红黑树进行优化上文已经提及。在JDK8中会使用到一个双向链表来维护红黑树中的元素,首先jdk8在转移某个位置上的元素时,会判断这个位置是不是一个红黑树,如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置。
扩容代码分析(如有纰漏,还望指正):
final Node<K,V>[] resize() {
// 保存旧的位桶数组 Node<K,V> table
Node<K,V>[] oldTab = table;
// 判断旧的是否为空,为空则赋值为零,不为空则赋值为旧的数组长度。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 保存旧的HashMap的负载因子
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过最大值,重新调整后赋值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 不超过最大值,则扩充为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 负载因子扩展为原来的两倍
newThr = oldThr << 1;
}
// 初始容量被置于阈值
else if (oldThr > 0)
newCap = oldThr;
// 零初始阈值表示使用默认值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
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"})
// 创建新的位桶数组对象,newCap为新的长度。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历bucket,把每一个bucket都移动到新的buckets中
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表。
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);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
文章参考:
- https://javaguide.cn/java/collection/hashmap-source-code.html#put-%E6%96%B9%E6%B3%95
- https://www.pdai.tech/md/java/collection/java-map-HashMap&HashSet.html