在项目开发中,HashMap是及其常用的数据结构。今天,我们一起来看看它的源码实现(本文源码来自JDK 1.8)。
HashMap有如下类注释:从中可知:
HashMap是Map接口基于哈希表的一种实现。哈希表基于数组实现,元素是Entry对象。HashMap中将Entry形成链表(或者红黑树),来解决哈希冲突。HashMap主要属性如下:java
代码解读复制代码// 默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,2^31
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表树化时的阈值,当向entry数量为8的桶put元素时,将引起链表树化
static final int TREEIFY_THRESHOLD = 8;
// 桶取消树化时的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 哈希表
transient Node<K,V>[] table;
// 哈希表尺寸
transient int size;
// 触发下次扩容时map中的entry数,取值为capacity * load factor,
int threshold;
// 负载因子
final float loadFactor;
// 对HashMap实例的修改次数
transient int modCount;
// Entry视图
transient Set<Map.Entry<K,V>> entrySet;
当哈希冲突严重时,键值对在哈希表中的分布将很不均匀,有些桶中没有元素,而有些桶中有很多元素;此时,查询性能将受到影响。
因此,不能等到HashMap中键值对数量,达到或超过哈希表长度时,才进行扩容。
使用loadFactor(小于1)衡量哈希表的饱和程度。要求size > capacity * load factor
时,就进行扩容。通过rehash来减少哈希冲突,以保证HashMap的性能。
嵌套类Node<K,V>
,即键值对,是单链表结构。树化时使用嵌套类TreeNode<K,V>
。
树化只为提高哈希冲突严重时,在桶中查找某个key的效率。红黑树本质是自平衡的二叉查找树。
使用分离链表法的哈希表:
创建HashMap时,可指定初始容量和loadFactor。如果不指定,则使用默认值。
也可用一个Map实例来创建新Map。
当指定initialCapacity
时,会通过计算得到tableSize。HashMap中哈希表长度,要求始终是2的次方数。便于使用&
与运算计算余数。
tableSize-1的二进制表示,除最高位外其余全是1;与key的hashCode做与运算,即可得到对哈希表长度的余数。如,tableSizeFor(12)=16
,tableSizeFor(32)=32
。
在HashMap的构造方法中,并没有立即初始化哈希表,而是在发生第一次put时,才创建哈希表。
先看hashCode计算,HashMap中做了优化。核心逻辑在putVal()
中,逻辑如下:
注意:
size > threshold
判断,不会导致无效扩容。如容量为16,负载因子为0.75,threshold=12;当插入第13个key时,才会触发扩容;afterNodeInsertion()
定义了插入entry后的额外动作,是一个扩展点。onlyIfAbsent
为true时,put操作将只插入新key,不覆盖已存在的key(除非旧value为null)。主要逻辑如下:
resize()
来触发哈希表的初始化。需要注意,resize时对链表也采用尾插法。
为什么resize能减少哈希冲突程度呢?
比如,在长度为16的哈希表中,在index=2的桶中,有key=2、18、34、82、98共5个key;
在resize后哈希表长度变为32,2、34、98将仍在index=2的桶中,而18、82会被移动到index=2+16的桶中。
从而降低了哈希桶中哈希冲突
get方法调用了getNode()
,在key不存在时返回null。
而getOrDefault()
在key不存在时,可以返回给定值。
实现很简单。
containsKey
时间复杂度是o(1)。而containsValue
是O(n),与size成正比。
只有向HashMap真的插入一个键值对时,才可能触发桶的树化。
当某个哈希桶中键值对数量大于等于7时,将尝试对链表树化:binCount
即桶中entry的计数但是,当哈希表长度小于64时,不会树化,而是扩容。
反树化实现是java.util.HashMap.TreeNode#untreeify。当哈希桶中键值对数量减少时,都可能触发反树化。如remove、resize等操作。
桶由树退化为链表的条件,是桶中entry数小于等于6时。
JDK7中,HashMap的桶不会树化,始终是链表结构。在JDK8中,HashMap引入了红黑树
扩容差异:
链表的插入方式: