JDK版本:13
参考
建议大家直接看这篇,写的太好了~
1 类图
HashMap
- 实现
java.util.Map
接口,继承java.util.AbstractMap
抽像类。 - 实现
java.io.Serializable
接口。 - 实现
java.lang.Cloneable
接口。
顺便看看Map的4个常用实现类
HashMap
:今天的主角Hashtable
:线程安全,但性能不如ConcurrentHashMap
,没必要继续使用了LinkedHashMap
:HashMap
的子类,保存了元素插入的顺序。TreeMap
:能够根据key排序。key必须实现Comparable
接口或构造TreeMap
时传入自定义比较器。
2 概览
HashMap底层是依靠数组+链表(jdk1.8后引入了红黑树)实现的。查询操作如何实现O(1)的时间复杂度是我们最关心的问题。
来看它的几个主要属性:
Node<K,V>[] table
哈希桶数组int threshold
扩容阈值float loadFactor
负载因子int size
保存的键值对的数量
简单来讲,HashMap内部维护了一个Node数组table
,数组上的每一个位置叫做一个哈希桶。那么如何利用数组的特性来存储键值对?
对key求hash值就可以把key转为一个整数,以此作为下标就可以存在数组里了。但还不够,数组长度不够用怎么办?
那就把hash(key)
对数组长度取模,用hash(key) % (table.length -1)
作为下标就好了。还是不够,不同的key计算出来的值一样怎么办?
这也就是哈希冲突了。HashMap采用了链表法处理。每一个哈希桶都对应一个链表,如果发生哈希冲突就把新的value放在链表末尾。这样如果一个链表过长,还是会影响性能。从java8开始做了优化,当链表太长时,就转为红黑树。
而扩容也是查询操作保持O(1)时间复杂度的重要手段,我们希望尽量每个桶里都只放了一个元素。threshold
是扩容阈值,指当size
超过threshold
时,HashMap会进行扩容。扩容阈值通过threshold = table.length * loadFactor
计算得到。扩容操作是非常消耗性能的,在初始化HashMap时,最好估算大小,用 HashMap(int initialCapacity)
方法构造,避免频繁进行扩容。
loadFactor
是对空间和时间的一种平衡选择。数据量相同的情况下,loadFactor
越小,HashMap占用的空间越大,但越不容易哈希冲突,查询效率越高。相反,loadFactor
越大,占用空间越小,查询效率越低。loadFactor
默认值0.75,除非在特殊情况下,不建议修改。
table
的长度总是2的n次方。这样hash(key) % (table.length -1)
可以写为hash(key) & (table.length -1)
,位运算要有更高的效率。
3 构造方法
3.1 默认构造方法 HashMap()
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
// 使用默认负载因子创建一个空的HashMap (table会在第一次使用时初始化,默认初始容量为16)
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
3.2 给定容量的构造方法 HashMap(int initialCapacity)
public HashMap(int initialCapacity) {
// 创建一个指定容量(会计算得到2的幂)、默认负载因子的HashMap
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3.3 给定容量和负载因子的构造方法 HashMap(int initialCapacity, float 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;
// 计算扩容阈值
this.threshold = tableSizeFor(initialCapacity); // 注意这个方法
}
/**
* 容量必须是2的幂,通过此方法计算得到大于给定容量的最小的2的幂
*/
static final int tableSizeFor(int cap) {
// 从二进制cap的最左边的1开始,全部设置为 1 ,得到 n ,这样 n + 1就是要求的值
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); // cap - 1 再计算避免cap假设刚好是8,但 n=16 这是不对的。
// cap 是 0 或 1 的时候 n 是 -1,此时返回 1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
3.4 HashMap(Map<? extends K, ? extends V> m)
public HashMap(Map<? extends K, ? extends V> m) {
// 设置负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// table 为空,还未初始化
if (table == null) {
// pre-size
// 由map大小和负载因子计算table大小
float ft = ((float)s / loadFactor) + 1.0F; // 因为下边(int)向下取整
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 新的容量大于扩容阈值,则计算新的扩容阈值
if (t > threshold)