前言
今天我们来学习Java中较为常用的集合类 HashMap。
另外说明一下,本文的 HashMap 源码是基于Jdk1.8版本的,如果没有特别说明的话,之后的集合类源码解析都是1.8的版本。
打开HashMap源码文件,可以看到它是继承自 AbstractMap,并实现了Java集合的根接口Map,以及Cloneable和Serializable接口,所以HashMap可以被序列化。
HashMap的底层结构是哈希表的具体实现,通过相应的哈希运算就可以很快查询到目标元素在表中的位置,拥有很快的查询速度,因此,HashMap被广泛应用于日常的开发中。理想的情况就是一个元素对应一个Hash值,这样的查询效果是最优的。
但实际这是不可能的,因为哈希表存在“hash (哈希) 冲突“ 的问题。当发生hash冲突时,HashMap采用 “拉链法“ 进行解决,也就是数组加链表的结构。在HashMap的代码注释中,数组中的元素用 “bucket” (中文读作 桶) 来称呼,而哈希函数的作用就是将key寻址到buckets中的一个位置,如果一个 bucket 有多个元素,那么就以链表的形式存储(jdk1.8之前单纯是这样)。
这是HashMap的存储结构图:
关于 “拉链法” 和 “hash冲突” 的知识点有疑问的读者可以看下我之前的文章 数据结构:哈希表以及哈希冲突的解决方案 。
为了方便,下文的 “bucket“ 都用 “桶“ 替代。
在具体学习源码之前,我们需要先了解两个HashMap中的两个重要参数,“初识容量” 和 "加载因子",
初识容量是指数组的数量。加载因子则决定了 HashMap 中的元素在达到多少比例后可以扩容 (rehash),当HashMap的元素数量超过了加载因子与当前容量的乘积后,就需要对哈希表做扩容操作。
在HashMap中,加载因子默认是0.75,这是结合时间、空间成本均衡考虑后的折中方案,因为 加载因子太大的话发生冲突的可能性会变大,查找的效率反而低;太小的话频繁rehash,降低性能。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
成员变量好了,前面说了那么多,现在开始深入源码学习吧,先了解一下HashMap的主要的成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75F; static final int TREEIFY_THRESHOLD = 8; static final int MIN_TREEIFY_CAPACITY = 64; transient HashMap.Node<K, V>[] table; transient Set<Entry<K, V>> entrySet; transient int size; int threshold; final float loadFactor;可以看出,HashMap主要的成员变量比较多,有些变量还初始化了值,下面一个个来做解释。
DEFAULT_INITIAL_CAPACITY:默认初识容量 1 << 4 ,也就是16,必须是2的整数次方。
DEFAULT_LOAD_FACTOR:默认加载因子,大小为0.75。
MAXIMUM_CAPACITY:最大容量, 2^ 30 次方。
TREEIFY_THRESHOLD :树形阈值,大于这个数就要树形化,也就是转成红黑树。
MIN_TREEIFY_CAPACITY:树形最小容量。
table:哈希表的链接数组,对应桶的下标。
entrySet:键值对集合。
size:键值对的数量,也就是HashMap的大小。
threshold:阈值,下次需要扩容时的值,等于 容量*加载因子。
loadFactor:加载因子。
介绍玩变量,下面介绍HashMap的构造方法。
四个构造方法HashMap共有四个构造方法,代码如下:
//加载默认大小的加载因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //加载默认大小的加载因子,并创建一个内容为参数 m 的内容的哈希表 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; //添加整个集合 putMapEntries(m, false); } //指定容量和加载因子 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); } //指定容量,加载因子默认大小 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }