1. 原理
HashMap 底层是数组 + 链表 + 红黑树。
数组我们很熟悉,支持随机访问,所以在最优情况下,即 HashMap 没有出现 hash 冲突,没有形成链表或红黑树结构,此时数据都存在数组中,get 方法的查询时间复杂度为 O(1) 。
链表查询时间复杂度 O(n) ,红黑树 O(logn)。底层的数据结构和对应的时间复杂度是我们研究的基础。
再来看看 HashMap 继承或实现了哪些类和接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
如代码所示,HashMap 实现了 Cloneable、Serializable,说明支持克隆和序列化。
**备注:**所有关于 HashMap 的分析都是基于 JDK1.8 。
2. 源码解析
先来看看常用的基本属性。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
int threshold;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
transient Node<K,V>[] table;
transient int size;
默认 bucket 数组长度为16,默认负载因子 0.75 ,默认 threshold = 16 * 0.75 = 12 。当数组长度达到 12 时开始扩容。(这都不知道的同学建议去面壁思过)
TREEIFY_THRESHOLD 是指链表长度达到 8 以后转化为红黑树,UNTREEIFY_THRESHOLD 则相反。
size 是 HashMap 中元素的总个数,要和 table 的 length 区分开。
我们着重来看一下 Node ,因为数据都存储在 Node 中。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
- hash: 根据存储元素的 key 的 hash 值
- next:因为是数组,肯定要存储下一个节点
2.1 构造函数
思考一个问题:HashMap 是在什么时候给 bucket 数组分配内存的?是 new HashMap() 的时候吗?带着问题我们来看看HashMap 的四种构造函数。
2.1.1 HashMap()
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
可以看到,无参构造函数只是把 DEFAULT_LOAD_FACTOR 的值(默认 0.75)赋值给 loadFactor,loadFactor 是给 HashTable 用的,所以可以理解为啥正事都没干。
2.1.2 HashMap(int initialCapacity)
一般我们实际使用过程中都是用的这个,在新建一个 HashMap 的时候先预估存储的元素的个数,假设个数为 n ,然后用 n 除以 0.75 ,假设算出的值是 m ,initialCapacity 取不小于 m 的 2 的幂次方对应的值。
举个例子:我现假设要往 HashMap 中存入 17 个值,17 / 0.75 = 22.67 。取不小于 22.67 的最近的 2