前言
书接上文,上一篇中对 Map 接口与 AbstractMap 抽象类进行了介绍与分析,本篇将对 Map 接口的最终实现类 HashMap 做介绍与分析。
Map 的继承结构图:
这里可以看到,HashMap 直接继承了 AbstractMap 抽象类,实现了 Map 接口,所以接下来可以直接查看 HashMap 的源码。
由于在 HashMap 中还涉及到树化(红黑树)的概念,但是在了解红黑树之前,还要先了解一下 AVL 树(平衡二叉树),B 树, 2-3-4 树的概念,所以这里先对这些树概念依次了解:
AVL(平衡二叉树)
在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(log n)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。 摘自 Wiki
平衡二叉树也是 Mysql 数据库 innoDB 索引中进化重要的一环,在 数据库篇 中将介绍与分析,此处只需要知道平衡二叉树是基于二叉树的基础上,限制每个节点的平衡因子(-1 到 1,通过左右子树的高度差,包括可能存在的子树)递归旋转得到的,它保证了平均与最差的复杂度都是 O(log n)。
B 树
在计算机科学中,B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree)一个节点可以拥有最少2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。摘自 Wiki
平衡二叉树也是 Mysql 数据库 innoDB 索引中进化重要的一环,在 数据库篇 中将介绍与分析,此处只需要知道 B 树平衡二叉树针对操作系统进行的优化,它拥有二叉树的特性,并且不再局限于二叉,而是可以根据操作系统一次文件操作的块大小拥有更多的子树。
2-3-4 树
2-3-4 树在计算机科学中是阶为 4 的B树。
大体上同B树一样,2-3-4 树是可以用做字典的一种自平衡数据结构。它可以在 O(log n) 时间内查找、插入和删除,这里的 n 是树中元素的数目。
2-3-4 树在多数编程语言中实现起来相对困难,因为在树上的操作涉及大量的特殊情况。红黑树实现起来更简单一些,所以可以用它来替代。摘自 Wiki
这里就与数据库中的 innoDB 开始不同了,数据库中的 B 树经过优化进阶为了 B+ 数,在 数据库篇 中将对 B+ 数进行介绍与分析,此处只需要知道 2-3-4 数是一颗阶数位 4 的 B 数,阶数就是一颗树可以拥有的子树的个数。拥有 B 树的所有特性,只是阶位 4。
红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",它现代的名字源于Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文。红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:它可以在 O(log n) 时间内完成查找,插入和删除,这里的 n 是树中元素的数目。
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用,如实时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为基础模板的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树实现。
红黑树在函数式编程中也特别有用,在这里它们是最常用的持久数据结构(persistent data structure)之一,它们用来构造关联数组和集合,每次插入、删除之后它们能保持为以前的版本。除了 O(log n) 的时间之外,红黑树的持久版本对每次插入或删除需要 O(log n) 的空间。
红黑树是2-3-4树的一种等同。换句话说,对于每个2-3-4树,都存在至少一个数据元素是同样次序的红黑树。在2-3-4树上的插入和删除操作也等同于在红黑树中颜色翻转和旋转。这使得2-3-4树成为理解红黑树背后的逻辑的重要工具,这也是很多介绍算法的教科书在红黑树之前介绍2-3-4树的原因,尽管2-3-4树在实践中不经常使用。
红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。
性质:
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
节点是红色或黑色。
根是黑色。
所有叶子都是黑色(叶子是NIL节点)。
每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些性质并使算法复杂。为此,本文中我们使用"nil叶子"或"空(null)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。
摘自 Wiki
简单路径是图论中的概念,就是一条没有重复节点的卢纶。
这里注意虽然每棵 2-3-4 树(4 阶 B 树)都有等价的红黑树,但是红黑树是二叉的。这里 Wiki 中提示了:要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。这样就保证了红黑树基本上是平衡的,而对于它的操作的效率基本是相等的。
对 HashMap 中设计到的树的概念了解这些就可以了,下面开始阅读 HashMap 源码:
/**
* 基于实现 Map 接口的 Hash table。这个实现类提供了所有可选操作,并且允许 null 值和 null 键。
* (HashMap 类大致与 HashTable 相同,除了它是线程不安全的并且允许 null。)这个类不保证了 map 的顺
* 序;
* 特指它不能保证顺序一直不变。
*
* 这个实现那类对于基础操作(get 和 put)提供了时间恒等的性能,假设在 hash 功能将元素正确地分布在不同的*
* 桶内。对于 collection 视图的迭代操作需要与 HashMap 实例(桶数)加上它的长度(键值映射数量)的“容
* 量”成比例的操作时间。因此,如果迭代性能很重要的话不要讲初始容量设置的太高是很重要的(或者加载因子太
* 低)。
*
* 有两个参数会影响一个 HashMap 实例的性能:初始容量和加载因子。容量是 hash table 中桶的个数,初始容量
* 就简单的是 hash table 在建立时的容量。加载因子是一个度量在 hash table 自动增加之前,它的容量有多满
* 测值。当 hash table 中条目的数量超过加载因子和当前容量的结合,hash table 被 rehashed(意味着,内
* 部数据结构被重新构造)来使 hash table 拥有大约两倍的桶数。
*
* 就如同一般的规则,默认加载因子(0.75)为时间与空间消耗的提供了一个很好的权衡。更高的值减少了空间开销但
* 是增加了查找开销(影响到了 HashMap 中的大部分操作,包括 get 和 put)。map 中条目数量的期望值与它的
* 加载因子应该在设置初始化容量的时候就考虑好,来最小化 rehash 操作的次数。如果初始化容量比条目除以加载因
* 子的最大值还要大,根本不会发生 rehash 操作。
*
* 如果 HashMap 实例中的多个映射关系要被排序,用一个很大的容量来建立它将允许映射排序比让它自动在需要扩大
* table 的时候 rehashing 更加高效。注意使用许多相同 {@code hashCode()} 的键会理所当然的降低任何
* hash table 的性能。为了改善碰撞,当键时 {@link Comparable} 的时候,这个类可以在键s中使用比较顺序
* 来帮助断绝。
*
* 注意这个实现类不是线程安全的。如果多个线程并发地访问一个 hash map,并且至少有一个线程结构性地改变了
* map,它必须从外部实现线程安全。(一个结构性改变是指任何添加或者删除一个或者多个映射关系;仅仅修改一个实
* 例已经包含的键关联的值不是一个结构性改变。)典型的实现方式是通过对于一个自然封装了 map 的对象进行线程安
* 全化。
*
* 如果不存在这样的对象,map 应该被使用
* {@link Collections#synchronizedMap Collections.synchronizedMap}
* 方法封装。这最好在创建的时候完成,来防止意外的非线程安全的访问 map:<pre>
* Map m = Collections.synchronizedMap(new HashMap(...));</pre>
*
* 所有类的 “collection 视图方法”返回的迭代器是 fail-fast 的:如果 map 在迭代器创建后的任何时间点发生
* 了结构性的改变,以除了通过迭代器自身 remove 方法的任何方式,迭代器将抛出一个 {@link
* ConcurrentModificationException}。因此,对于并发修改,迭代器失败的快速与干净,而不是在未来某个不
* 确定的时间点冒险做任何不确定的行为。
*
* 注意一个迭代器的 fail-fast 行为不能提供像它所描述的那样的保证,一般来说,不可能对于不同步的并发操作做
* 任何硬性的保证。基于最好性能的基础,fail-fast 迭代器抛出一个 ConcurrentModificationException。
* 因此,编写一个依赖于这个异常来保证其正确性的程序是错误的:迭代器的 fail-fast 行为应该只被用于检查
* bugs。
*
* 这个类的注释先后改了三次,因为其中有太多难以理解的细节了,全部写上反而会因为无用信息太多而更难以理解,再
* 加上本人理解与翻译能力有限,所以优化了三次注释,目前只对重要的类,方法,属性,以及重要的实现部分做标注,
* 重复的地方也不再标注。对于方法与类属性,标注了方法与属性的意义,以及对于 JavaDoc 的翻译,翻译可能不
* 对,但是意义应该没有问题,对于特定名称保留英文名称。
*
* HashMap 对于在条目上使用了数组 + 链 + 红黑树三种结构,对于树化节点与容器的操作的实现方式非常复杂,细
* 节不做翻译(因为要搞清楚这些代码的成本有点高。。)
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
/*
* 实现类注意(这一段我已经很努力翻译了,但是由于对于实现的不理解,所以翻译的还是比较差,可以跳过不
* 看)
*
* 这个 map 通常充当一个桶化的 hash table,但是当桶变得太大时,它们会在容器中转化为 TreeNodes,每
* 一个都被构造成类似于那些 java.util.TreeMap 的结构。大部分方法在适用的时候(简单地通过检查一个
* node 的实例)尽力去使用普通的容器,而不是转发到 TreeNode 方法。TreeModes 的容器可以想其他(的
* 容器)一样被穿过和使用,但是额外支持(容器)过多时候的更快速的查找。但是,既然在平时使用过程中大多
* 数容器是没有过多的,在 table 方法(调用)期间可以延迟检查 tree 容器的存在。
*
* Tree 容器(比如,元素都是 TreeNodes 的容器)首先通过 hashCode 排序,但是当发生碰撞的时,如果两
* 个元素同样是 “class C immplements Comparable<C>” 类型那么他们的 compareTo 方法被用来排
* 序。(我们谨慎地通过反射检查泛型来验证这个--查看 comparableClassFor 方法)。tree 容器增加的复
* 杂度在当键既不是唯一 hashs 或者有序的时候提供最差情况 O(log n) 操作是值得的,因此,性能在
* hashCode() 方法返回的值被分散的很差时意外或者故意使用(容器)时性能优雅的降低,以及很多键分享一个
* hashCode 的情况,只要它们也是 Comparable 的。(如果这些中没有一个应用了,与不采取预防措施相比,
* 我们可能浪费了大概一到两倍的时间和空间。但是对于已知的来自于糟糕的用户编程实践而引起的缓慢来说,这
* 影响很小。赤裸裸的嘲讽。。)
*
* 因为 TreeNode 差不多是普通 nodes 的两倍大小,我们只有在容器为了保证使用(查看
* TREEIFY_THRESHOLD)包含了足够多的 nodes 时才使用它们。并且当它们变得太小(因为移除或者重建)它
* 们将被重新转换成一个普通的容器。在于很好的分布了的用户 hashCodes 一起使用时。tree 容器是很少被使
* 用的。理想的是,在随机 hashCode 下,容器中的 nodes 的频率跟随一个泊松分布
* (http://en.wikipedia.org/wiki/Poisson_distribution)对于默认重构门槛 0.75,平均参数大概
* 是 0.5,虽然由于重构颗粒会伴随一个巨大的变动。无视变动,期望的 list 大小的发生是 (exp(-0.5) *
* pow(0.5, k) /factorial(k))。第一值是:
*
* 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
* 更多:这一千万种种比 1 更小
*
* 一个 tree 容器的跟通常是它的第一个 node。但是,有时候(目前只在 Iterator.remove 上),根可能
* 在别处,但是可以被以下父类链接 (method TreeNode.root())。
*
* 所以内部应用方法接收一个 hash 值作为一个参数(通常有一个 public 方法提供),允许它们调用对方而不
* 用重新计算用户 hashCodes。大部分内部方法也接收一个 “tab” 参数,它通常是当前 table,但是可能在
* 重构或者转换的时候变成一个新的或者旧的。
*
* 当容器链表是 treeified,分隔,或者 untreeified,我们将它们保持在相同的访问/穿过顺序(比如,类
* 属性Node.next)来更好的保护线程,并且来将分隔和穿过的操作与调用 iterator.remove 稍微简单化。当
* 在插入种时候 comparators,来通过再平衡保证一个完整的顺序(或者与这里需要的尽量接近),我们比较类
* s与相等的 Hash 值作为冲突解决。
*
* 一般的和 tree 模式下的使用和转变的区别被子类 LinkedhashMap 的存在复杂化了。查看以下的 hook 方
* 法,这些方法定义为在插入,移除和访问时调用,从而允许 LinkedHashMap 内部以其它方式保持对于这些机
* 制的独立性。(这还需要传递一个 map 实例给可能创建新 nodes 的方法。)
*
* SSA 基础的并行编程式样编程风格帮助避免在所有扭曲指针中出现混叠错误。
*/
/**
* 注意:以下变量的选择都涉及到各种复杂的权衡,这里不必完全理解,只要知道他们是什么,以及作用是什
* 么就可以了
**/
/**
* 默认初始化容量
*
* 必须是 2 的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量
*
* 在无论是构造器的哪个参数隐式的被指定是更大的值的时候使用。
* 必须是 2 的 <= 1<<30 次方。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子
*
* 当没有指定在构造器中指定时使用的加载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化阈值(对应于转换为 TreeNode)
*
* 容器用以树而不是一个列表来计算容器的阈值。在添加一个元素到至少有这么多 nodes 的容器中时容器被转换
* 成树。该值必须大于2,并且至少应为8,以符合树移除中关于收缩后转换回普通容器的假设。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 种化阈值(对应于转换为普通 Node)
*
* 为了在一个重构操作中解冻一个(拆分)容器的容器数量阈值。应该比 TREEIFY_THRESHOLD 小,且在配合在
* 移除操作下缩小检查最多为 6。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小树化容量
*
* 对于需要被 treeified 的容器的最小 table 容量。(否则如果在容器中有太多的 nodes table 被重
* 构。)应该是至少 4 * TREEIFY_THRESHOLD 来避免重构与树化阈值之间的复杂性。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 重要内部类 Node
*
* 基础的 hash 容器 node,被大多数条目使用。(在下面查看 TreeNode 子类,并在 LinkedHashMap 中查
* 看 Entry 子类。)实现了 Map.Entry 接口,是普通条目(树化之前)类。注意在 Entry 接口的源码中可
* 以看到它是一个单独的对象,而实现它的 Node 就可以看到是一个链式对象。
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //hash
final K key; //键
V value; //值
Node<K,V> next; //下一个,所以 Node 的结构类似于单向链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
public final String toString() {
return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
/**替换条目中的值**/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue())) //分别比较键和值的相等性
return true;
}
return false;
}
}
/* ---------------- 静态工具 -------------- */
/**
* 计算 key.hashCode() 并且散开(XORs)较高的 hash 的比特位到较低。因为 table 使用 2 的幂次方掩
* 码,仅在当前掩码上方以位为单位变化的哈希集将始终发生冲突。(在已知的例子中,一组浮动键在小表中保持
* 连续的整数。)所以我们应用了一种将较高的比特位向下降低的转换。在速度,工具性和比特的传播性之间有一
* 个权衡。因为很多一般的 hahes 的 sets 已经明智的分布了(所以从传播中收益),而且因为我们使用
* trees 来解决容器中的巨大的 sets 的碰撞,我们只要以最廉价的可能的方式方式 XOR 一些改变过的比特值
* 来减少语义的损失,也能合并最高的因为 table 接线而永远不会在索引计算中被使用到的比特位的影响。
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 如果 x 的类型是符合 “class C implements Comparable<C>” 则返回它的类型,否则返回 null。
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
/***类 c,类型数组 ts,as,类型 t,范型类型 p**/
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) //分支检查
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) //c 是泛型类型参数
return c;
}
}
}
return null;
}
/**
* 如果 x 的类型是符合 “class C implements Comparable<C>” 则返回它的类型,否则返回 null。
*/
@SuppressWarnings({
"rawtypes","unchecked"}) //为了转换成 Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 : //这里需要将 x 的类与 kc 做比较,不明白为,比价成功才会调用 compareTo 方法,不明白为什么需要 kc。。
((Comparable)k).compareTo(x));
}
/**
* 返回给定对象容量的一个长度(2 的幂次方)。这里是一些算法
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/* ---------------- 类属性 -------------- */
/**
* 当前 HashMap 持有的 Node 数组(包含 Node 和 TreeNode)
*
* Table,在第一次使用时被初始化,并在需要是重构。当分配后,长度总是 2 的幂次方。(我们在一些操作中
* 也容许 0 长度来允许现在已经不需要的引导算法。)
*/
transient Node<K,V>[] table;
/**
* 当前 HashMap 持有的 Map.Entry(条目) set
*
* 持有缓存的 entrySet()。注意 AbstractMap 的类属性被 keySet() 和 values() 使用。之前在
* AbstractMap 中看到的最重要的属性 entrySet
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 当前 HashMap 的长度
*
* 当前 map 中包含的键值映射数量。
*/
transient int size;
/**
* 结构性改变次数
*
* 当前 HashMap 已经被结构性修改的次数,是哪些更高了 HashMap 中映射的数量或者不然就是修改了它的内
* 部结构(比如 rehash)。这个类属性被用来建立 HashMap 的 Collection 视图的迭代器 fail-fast。
* (查看 ConcurrentModificationException)。
*/
transient int modCount;
/**
* 当前 HashMap 的阈值
*
* 下一个重构的大小值(容量 * 加载因子)
*
* @serial
*/
// (这个 java 文档描述是真正的序列化。另外,如果 table 数组没有被分配。这个类属性持有初始化数组容量,或者 0 表示 DEFAULT_INITIAL_CAPACITY。)
int threshold;
/**
* 当前 hash table 的加载因子
*
* @serial
*/
final float loadFactor;
/* ---------------- 公共操作 -------------- */
/**
* HashMap 双参(初始化容量和加载因子)构造方法
*
* 使用指定的初始化容量和加载因子构造一个空的 HashMap。
*/
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);
}
/**
* HashMap 单参(初始化容量)构造器
*
* 使用指定初始化容量和默认加载因子(0.75)构造一个空的 HashMap。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* HashMap 无参构造方法
*
* 使用默认初始化容量(16)和默认加载因子(0.75)构造一个空的 HashMap。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; //所有其他类属性都是默认的
}
/**
* HashMap 单参(另一个 Map)构造方法
*
* 使用一个指定 Map 相同的映射s构造一个新的 HashMap。HashMap 被使用默认加载因子(0.75)和足够装下
* 指定 Map 映射s的初始化容量构造。
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* 塞入所有条目方法
*
* 实现 Map.putAll 和 Map 构造器(单参(另一个 Map)构造方法)。
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
//检查 m 的长度
if (s > 0) {
if (table == null) {
//在 size 之前
float ft = ((float)s / loadFactor) + 1.0F;
//缓存容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//根据容量计算阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize(); //容量大于阈值,重构当前 map
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
//遍历参数 map,塞入当前 map
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
/**
* 或当当前 HashMap 长度方法
*
* 返回当前 map 中键值映射的数量。
*/
public int size() {
return size;
}
/**
* 判空方法
*
* 如果当前 map 不包含键值映射,返回 true。
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 键查值方法
*
* 返回指定键映射到的值,或者如果 map 中欧冠不包含这样的键就返回 null。
*
* 更正式地说,如果 map 包含一个这样的键 k 到这样的值 v: {@code (key==null ? k==null :
* key.equals(k))},那么这个方法返回 v;不然的话它返回 null(至多只有一个这样的映射。)
*
* 一个 null 返回值不是必然意味着 map 中不包含这个键的映射;它也可能明确地标示存在键到 null 的映射。#containsKey 操作可以用来区分这两种情况。
*/
public V get(Object key) {
//获取 Node,返回 Node 的值
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 获取 Node 方法
*
* 实现 Map.get 和相关方法。
*
* 方法中重要实现片段 tab[(length - 1) & hash] 参考了
* https://www.jianshu.com/p/2d86f83d1d26 文章,此处做下搬运,这样计算索引要插入的位置好处主要
* 有两个:
* 1、时一定不会发生数组越界,因为 table 的长度规定时间 2 的幂次方,转成二进制一定是 1000...0(1
* 后面偶数个0),那么 length - 1 一定是 0111...1(0 后面偶数个1),这时候发生位与结果值一定不会
* 比原值大,所以是一定不会越界的。
* 2、使得元素分布更加均匀,由之前的分析可知,table 的长度一定是偶数,length - 1 一定是奇数,假设
* 现在数组的长度(table.length)是 16,减去 1 (length - 1)就是 15,15 对应的二进制值时
* 1111。现在有两个元素需要插入,一个 Hash 值为 8(二进制值时 1000),一个为 9(二进制值时
* 1001),那么它们与(length - 1)位与后就是 1000 和 1001,被分配在数组的不同位置,这样是比较均
* 匀的。如果数组长度是奇数,比如 15,那么 length - 1 就是 14,对应二进制值时 1110,对于上面两个
* Hash 值位与后得到的结果都是 1000,它们被分配在数组的相同位置,不够均匀。
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断并计算要插入数组的位置值
if (first.hash == hash && //总是检查第一个 Node(这里检查第一个的意思相当于当前桶中只有一个数据,这是大概率时间,如果命中直接返回)
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//到了这一步已经确定 Node 存在并且 Node 的首元素不是需要的元素了
if (first instanceof TreeNode) //判断 Node 是否为 TreeNode,如果是,则执行获取 TreeNode 逻辑
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//到这里,Node 就有 next,又不是 TreeNode,通过 e.next 循环遍历 Node,直到找到需要的结果
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
/**
* 判存方法(键)
*
* 如果当前 map 包含一个指定键的映射,返回 true。
*/
public boolean containsKey(Object key) {
//直接调用 getNode 判断结果是否存在
return getNode(hash(key), key) != null;
}
/**
* 塞入/更新方法
*
* 将当前 map 中的指定键与指定值连接。如果 map 之前包含一个键的映射,旧值将被替换。
*/
public V put(K key, V value) {
//直接调用 putVal 方法
return putVal(hash(key), key, value, false, true);
}
/**
* 塞值方法
*
* 实现 Map.put 和相关方法。为什么不直接写在 put 方法里?这种设计的意义没有明白。。
*
* onlyIfAbsent 参数的含义是是否要更换值,如果为 true,则不改变已经存在值。
* evict 参数的含义是调用时间,如果在创建 HashMap 时候调用则为 true,创建后调用为 false。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这种多用局部中间变量的方式可以理解为作者的编码风格,这样做的好处是
//1、中间变量,用于暂存,原值可能被替换
//2、成员变量皆为全名,局部变量可用缩写
//3、配合变量在方法中实际的含义重命名
//4、便于调试
Node<K,V>[] tab; Node<K,V> p; int n, i;
//若 Node 数组为空或者 Node 数组中没有元素,重构计算长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //如果 Node 数组中 hash 对应的位置的 Node 为空
tab[i] = newNode(hash, key, value, null); //构造 Node 并插入数组最后位置
else {
Node<K,V> e; K k;
if (p.hash == hash && //此处已经肯定 Node 数组中 hash 对应的位置的 Node 不为空
((k = p.key) == key || (key != null && key.equals(k)))) //如果这个 Node 的键与参数相同,还是检查 Node 的首元素
e = p; //e 就不为 null 了
else if (p instanceof TreeNode) //如果该 Node 是 TreeNode 实例
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //调用 TreeNode 的塞值方法
else {
//否则说明该 Node 存在 next
for (int binCount = 0; ; ++binCount) {
//容器累加器
if ((e = p.next) == null) {
//链式遍历结束,将这个判断写在前面是因为这种情况 Node 存在后续节点的情况不常发生,如果当前遍历到的 Node 依然存在后续节点,则走下面的 if,直到最后一个 Node,如果都没有找到,那么进入这个 if 分支时 e 一定是 null,意味着没有找到对应的映射
p.next = newNode(hash, key, value, null); //构造新 Node 并链接在当前遍历到的 Node 之后
if (binCount >= TREEIFY_THRESHOLD - 1) //判断当前累加器是否大于等于树化阈值 - 1,第一个给 -1
treeifyBin(tab, hash); //树化容器
break; //由于已经是链的最后一环,跳出
}
if (e.hash == hash && //此时 e 已经是遍历链的后一个 Node
((k = e.key) == key || (key != null && key.equals(k)))) //如果 e 的键与参数相同
break; //跳出循环
p = e; //走到这里说明没有遍历到最后一环 Node,并且当前 Node 的键与参数不同,不是同一个 Node,更新步移参数 p
}
}
if (e != null) {
//根据上面的分析,存在键对应的映射时,e 一定不是 null
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //旧值可以改变或者旧值是 null
e.value = value; //改变 e 的旧值
afterNodeAccess(e); //这个方法还没出现,等待后续分析
return oldValue; //返回旧值
}
}
++modCount; //发生了结构性的改变,替换旧值时会直接返回 oldValue,其他情况都属于在创建或者添加后续节点,结构都发生了改变,所以需要自增 modCOunt 和 size
if (++size > threshold) //自增长度,如果长度大于阈值
resize(); //重构当前 HashMap
afterNodeInsertion(evict); //这个方法还没出现,等待后续分析
return null; //没有旧值,返回 null
}
/**
* 重要方法,重构方法
*
* 初始化 table 长度或者将 table 长度翻倍。如果是 null,使用类属性 threshold 持有的初始化容量对
* 象来分配。不然的话,因为我们使用 2 的幂次方扩展,每个容器中的元素必须要么是在新的 table 中保持在
* 相同索引位置,要么是移动到 2 的幂次方的偏移量。
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//旧容量就是旧数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果旧容量大于 0
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE; //如果旧阈值大于 MAXIMUM_CAPACITY,限制为 Integer.MAX_VALUE
return oldTab; //直接返回旧数组
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //如果旧阈值翻倍后小于 MAXIMUM_CAPACITY 且旧阈值大于等于 DEFAULT_INITIAL_CAPACITY
newThr = oldThr << 1; //新阈值等于旧阈值翻倍,如果旧阈值为 0,那么新阈值还是 0,情况 1
}
else if (oldThr > 0) //次数旧长度小于等于 0,说明是初始化,但是旧阈值大于 0
newCap = oldThr; //用旧阈值作为初始化容量
else {
//如果旧容量小于等于 0,旧阈值也小于等于,表示新容量与新阈值都使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//如果新阈值等于 0,也就是上述判断种的情况 1
float ft = (float)newCap * loadFactor; //新容量乘以加载因子,作为新阈值缓存
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); //在加载因子大于 1 的情况下会出现 ft 比新容量大的情况,所以两个都要与 MAXIMUM_CAPACITY 比较做限制,计算新阈值
}
threshold = newThr; //替换当前 HashMap 的阈值
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //分配空的 Node 数组
table = newTab; //替换当前 HashMap 的 Node 数组
if (oldTab != null) {
//当旧 Node 数组不为 null 时候
for (int j = 0; j < oldCap; ++j) {
//遍历数组
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//如果遍历到的当前 Node 不为空
oldTab[j] = null; //当前 Node 置为 null
if (e.next == null) //依然判断 Node 首元素,如果没有后续节点
newTab[e.hash & (newCap - 1)] = e; //塞入新数组中重新定位的位置
else if (e instanceof TreeNode) //或者如果 e 是 TreeNode 实例
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //调用 TreeNode split 方法,这个方法还没有出现,后续分析
else {
//此处说明 e 有后续节点,并且 e 不是一个 TreeNode
Node<K,V> loHead = null, loTail = null; //这里可以认为 loHead 与 loTail 是 lo 链条上的头与尾
Node<K,V> hiHead = null, hiTail = null; //这里可以认为 hiHead 与 hiTail 是 hi 链条上的头与尾
Node<K,V> next;
do {
next = e.next;
//奇怪这里为什么不用 oldCap - 1。。
if ((e.hash & oldCap) == 0) {
//由于 Node 数组的长度肯定是 2 的幂次方,以 16 为例(二进制 10000),假设 e 的哈希值为 9(二进制 01001),位与得 0,如果 e 的哈希值为 17(二进制 10001),位与得 10000,对元素进行区分
if (loTail == null) //如果 lo 链尾为 null
loHead = e; //将当前 Node 赋值给 lo 链头,情况 2
else //如果 lo 链尾有 Node
loTail.next = e; //将当前 Node 拼接在 lo 链尾后,情况 3
loTail = e; //将当前 Node 赋值给 lo 链尾,这里是为了辅助检查 loTail == null,将在循环结束后置为 null,类似于拼接“,”,循环结束后移除最后一个 ","
}
else {
//当前 Node 不在原数组中的第一个位置
if (hiTail == null) //如果 hi 链尾为 null
hiHead = e; //当前 Node 赋值给 hi 链头
else //如果 hi 链尾有 Node
hiTail.next = e; //将当前 Node 拼接在 hi 链尾后
hiTail = e; //将当前 Node 赋值给 hi 链尾,与情况 4 相同
}
} while ((e = next) != null); //循环条件为当前 Node 存在后续节点
//这里就是在原数组 j 位置的 Node,重新分割到了新数组的 j 与 j + oldCap 位置上,更分散了