前言
本文基于 jdk 1.8 分析 HashMap 源码,主要内容有
- 基础知识简介
- HashMap 的底层存储详解
在 jdk1.8 中,HashMap 使用了红黑树来优化,若要彻底理解 HashMap 的相关知识,可以先了解红黑树的相关内容,然后再阅读本文。
主要参数
本小节主要介绍 HashMap 的核心参数及其值设定原理。
-
loadFactor
loadFactor 负载系数(也称负载因子),默认值为 0.75。该值决定了一个 HashMap 实例对象的存储容量将在何时进行扩容。举个简单但不是那么恰当的例子,如图 1 将 hash 表看成是一个装水的桶,在往桶中加入水,当水的体积到达桶的容量的 75% 时则需要更换更大的桶来装水。更换桶后需将小桶的水加入到大桶中。
图 1 那么 0.75 值是否是固定不可变?
0.75 是 jdk 设定的默认值,定义该值是权衡时间复杂度和空间复杂度的平衡点而考虑的,该值可在创建 HashMap 实例时通过传入参数改变,其取值范围为 0 ~ 1。
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
若实例化 HashMap 时未指定负载系数,将使用默认的值 0.75。负载因子越大,hash 表中可装的元素越多,空间利用率高,但带来的问题是 hash 冲突增加,由于 HashMap 中采用的是拉链法解决 hash 冲突,当冲突增加时,桶中的链表将增加,导致查询性能降低。负载因子越小,hash 表中可装的元素越少,虽减少了 hash 冲突,但需要频繁扩容导致空间利用率不高浪费资源。
实际使用过程中,可依据业务需求适当更改,不过大部分情况下,使用默认值即可。
为何一定是 0.75?
注意看源码中给出的很关键的一个词——
Poisson distribution
(泊松分布)。* Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a **Poisson distribution** * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006
在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。从表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用 0.75 作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
-
TREEIFY_THRESHOLD & MIN_TREEIFY_CAPACITY
这两个参数定义了 hash 表的桶的链表转红黑树的阈值,取值分别为 8,64。只有当桶中的链表长度 >= 8 且 hash 表中元素的数量 > 64 时,才会将链表转化为红黑树。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 链表长度大于 8 后,尝试转化为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 再次 hash 表的元素是否大于等于最小元素转化数量 64,不满足则继续扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
为何是 8、64 的取值?
链表转化为红黑树是一个复杂的过程,且红黑树的数据结构 TreeNode 所需内存是链表 Node 的两倍,费时且费空间。深层次的原因还是基于泊松分布的概率计算而来,而不是拍脑门子决定。
-
UNTREEIFY_THRESHOLD
该属性定义了有红黑树转化为列表的值,默认为 6。即当红黑树的节点数为 6 是,将桶的数据结构将由红黑树转化为链表。为何取值为 6?
存储详解
存储结构
在 jdk1.8 中,HashMap 存储数据使用的数据结构是数组 + 链表 / 红黑树。
- 链表节点数据结构为
static class Node<K,V> implements Map.Entry<K,V> { final int hash; // hash 值 final K key; // map 中的 key V value; // map 中的 value Node<K,V> next; // 下一个 node }
- 红黑树节点数据结构为
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<