<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans-CN">
	<id>https://wiki.riguz.com/index.php?action=history&amp;feed=atom&amp;title=Blog%3A%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%9AHashMap</id>
	<title>Blog:阅读笔记：HashMap - 版本历史</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.riguz.com/index.php?action=history&amp;feed=atom&amp;title=Blog%3A%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%9AHashMap"/>
	<link rel="alternate" type="text/html" href="https://wiki.riguz.com/index.php?title=Blog:%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%9AHashMap&amp;action=history"/>
	<updated>2026-06-03T00:55:51Z</updated>
	<subtitle>本wiki上该页面的版本历史</subtitle>
	<generator>MediaWiki 1.42.3</generator>
	<entry>
		<id>https://wiki.riguz.com/index.php?title=Blog:%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%9AHashMap&amp;diff=2615&amp;oldid=prev</id>
		<title>imported&gt;Riguz：​HashMap可以算是最常用的数据结构了，而它的实现没想到还挺有学问在里面。</title>
		<link rel="alternate" type="text/html" href="https://wiki.riguz.com/index.php?title=Blog:%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%9AHashMap&amp;diff=2615&amp;oldid=prev"/>
		<updated>2020-03-18T00:00:00Z</updated>

		<summary type="html">&lt;p&gt;HashMap可以算是最常用的数据结构了，而它的实现没想到还挺有学问在里面。&lt;/p&gt;
&lt;p&gt;&lt;b&gt;新页面&lt;/b&gt;&lt;/p&gt;&lt;div&gt;HashMap可以算是最常用的数据结构了，而它的实现没想到还挺有学问在里面。&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= 基本实现=&lt;br /&gt;
== 哈希映射==&lt;br /&gt;
在HashMap中使用数组来存储元素，根据元素的hash值一一映射到一个节点上。其中使用的哈希方法为：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;java&amp;quot;&amp;gt;&lt;br /&gt;
static final int hash(Object key) {&lt;br /&gt;
    int h;&lt;br /&gt;
    // 将哈希值无符号右移16位是因为取index使用了length作为掩码，这样当哈希值在掩码外的部分相同的时候就会发生冲突&lt;br /&gt;
    // 这样将高位混杂到低位上，可以尽可能将这种影响消除&lt;br /&gt;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
举例来说对于容量为4的HashMap，插入&amp;quot;a&amp;quot;、&amp;quot;b&amp;quot;、&amp;quot;c&amp;quot;、&amp;quot;d&amp;quot;后在数组中的分布就是：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;lua&amp;quot;&amp;gt;&lt;br /&gt;
&amp;quot;a&amp;quot;.hashCode() = 97 = 00000000000000000000000001100001&lt;br /&gt;
hash(&amp;quot;a&amp;quot;) = 00000000000000000000000001100001 ^ 00000000000000000000000000000000&lt;br /&gt;
          = 00000000000000000000000001100001&lt;br /&gt;
index = hash(&amp;quot;a&amp;quot;) &amp;amp; (4 - 1)&lt;br /&gt;
      = 00000000000000000000000001100001 &amp;amp; 00000000000000000000000000000011&lt;br /&gt;
      = 00000000000000000000000000000001&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
如此则数组中对应的序号为1，2，3，0。&lt;br /&gt;
&lt;br /&gt;
[[File:HashMap-resize.png|600px|HashMap resize]]&lt;br /&gt;
&lt;br /&gt;
== Load Factor(负载因子)和Threshod（阈值）==&lt;br /&gt;
因为HashMap的底层实际上是使用数组进行存储，那么始终存在着一个动态内存分配的问题：数组的大小是固定的，但是HashMap实际存储多少数据是未知的（可以一直向HashMap中进行插入），那么当数组塞满了（实际上还有一个问题是发生哈希冲突）之后如何处理？&lt;br /&gt;
&lt;br /&gt;
解决这个问题最简单的做法就是，一旦数组满了之后，就对数组进行扩容。扩容也很简单，重新申请一个大一点的数组，再把原来数组里面的数据复制过去即可。这里涉及到另外一个问题就是，扩容的时候选择一个怎么样的容量进行扩容呢？这个操作是有代价的，如果频繁的扩容就涉及到频繁的数组复制操作，性能上会受到影响；如果一次扩容选择一个很大的空间，但实际之后这些空间又没有使用到，那么久造成了资源浪费。怎么解决这一个问题呢？&lt;br /&gt;
&lt;br /&gt;
在HashMap的构造中有两个关键的参数：&lt;br /&gt;
&lt;br /&gt;
* `initialCapacity`:初始化容量，即可以装多少条数据&lt;br /&gt;
* `loadFactor`：负载因子，用来描述HashMap中可以变得多“满”（到达什么程度开始扩容）&lt;br /&gt;
&lt;br /&gt;
实际上，HashMap并不会根据你提供的`initial capacity`来初始化一个数组，而是找到一个值 $t$ 并满足 $t &amp;gt;= i \&amp;amp;\&amp;amp; t==2^{n}$（比如3对应得到4， 15对应得到16），并在第一次插入的时候进行初始化。&lt;br /&gt;
&lt;br /&gt;
为什么数组在初始化的时候一定是2的倍数？这是因为方便扩容的时候直接将数组大小变成原来的二倍，同时也简化了一些其他的操作，比如如何定位到一个值所在的索引:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;java&amp;quot;&amp;gt;&lt;br /&gt;
int index = (length - 1) &amp;amp; hash&lt;br /&gt;
&lt;br /&gt;
/*&lt;br /&gt;
final Node&amp;lt;K,V&amp;gt; getNode(int hash, Object key) {&lt;br /&gt;
    Node&amp;lt;K,V&amp;gt;[] tab; Node&amp;lt;K,V&amp;gt; first, e; int n; K k;&lt;br /&gt;
    if ((tab = table) != null &amp;amp;&amp;amp; (n = tab.length) &amp;gt; 0 &amp;amp;&amp;amp;&lt;br /&gt;
        (first = tab[(n - 1) &amp;amp; hash]) != null) {&lt;br /&gt;
    ...&lt;br /&gt;
*/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
正常的做法是，`abs(hash) % SIZE`像这样取余操作。但是如果除数是2的n次幂，则可以简化为位运算操作。&lt;br /&gt;
&lt;br /&gt;
而至于为什么默认的负载因子是0.75，有人根据二项式分布算出最佳的load factor是 $log(2)=0.693$ ，然后拍脑袋给出的0.75（乘以容量还可以得个整数...)。&lt;br /&gt;
&lt;br /&gt;
= 树化（红黑树）=&lt;br /&gt;
== TREEIFY_THRESHOLD（树化阈值）==&lt;br /&gt;
所以使用0.75作为负载因子，那么出现的情况是如果当前容量达到这个值的时候就会resize到原来的两倍。对于一个容量为4的Map来说，理想情况下元素均匀分布，是这样：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
最好情况                                 极端情况&lt;br /&gt;
bucket | elements                      bucket | elements     &lt;br /&gt;
-------+---------                      -------+---------    &lt;br /&gt;
     0 | Z                                  0 |   &lt;br /&gt;
     1 | X                                  1 | Z -&amp;gt; X -&amp;gt; Y &lt;br /&gt;
     2 |                                    2 |  &lt;br /&gt;
     3 | Y                                  3 | &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
理想状况下（假设基于随机hash算法节点在桶中均匀分布，且节点的个数占桶的50%，那么单个节点出现在桶中的概率为0.5），节点在hash桶中的出现的频率遵循[https://zh.wikipedia.org/wiki/%E6%B3%8A%E6%9D%BE%E5%88%86%E4%BD%88 泊松分布]（ $λ = 0.5$ )&lt;br /&gt;
&lt;br /&gt;
$$&lt;br /&gt;
P(X=k)=\frac{e^{-\lambda}\lambda^k}{k!}=\frac{e^{-0.5}0.5^k}{k!}&lt;br /&gt;
$$&lt;br /&gt;
&lt;br /&gt;
意味着在load factor=0.75的情况下，hash桶中出现 $k$ 个节点（冲突）的概率大致为：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* 0:    0.60653066&lt;br /&gt;
* 1:    0.30326533&lt;br /&gt;
* 2:    0.07581633&lt;br /&gt;
* 3:    0.01263606&lt;br /&gt;
* 4:    0.00157952&lt;br /&gt;
* 5:    0.00015795&lt;br /&gt;
* 6:    0.00001316&lt;br /&gt;
* 7:    0.00000094&lt;br /&gt;
* 8:    0.00000006&lt;br /&gt;
* more: less than 1 in ten million&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
可见哈希冲突导致一个桶中出现8个节点情况已经几乎小之又小的事情了，这是`TREEIFY_THRESHOLD = 8`的原因，当大于8的时候转换为红黑树。&lt;br /&gt;
&lt;br /&gt;
== Treeify（树化）==&lt;br /&gt;
通常情况下，当哈希冲突产生的时候，会被当成链表存储。这个改变是通过[http://openjdk.java.net/jeps/180 JEP 180: Handle Frequent HashMap Collisions with Balanced Trees]引入的。在下面的情况下，会转换为红黑树：&lt;br /&gt;
&lt;br /&gt;
* 链表中的节点数达到TREEIFY_THRESHOLD（8）&lt;br /&gt;
* 容量至少达到MIN_TREEIFY_CAPACITY（64），否则只是单纯扩容到到原来的两倍&lt;br /&gt;
&lt;br /&gt;
现实中哈希冲突的场景并不多，不过如果非要测试这种场景也很容易。比如`Aa`和字符串`BB`就拥有相同的哈希值，把他们随机组合到一起，还是一样。于是我们构建了很多个哈希值相同的key值，来演示哈希冲突的场景：&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:HashMap-treeify.png|600px|Treeify]]&lt;br /&gt;
&lt;br /&gt;
== 尾插入==&lt;br /&gt;
&lt;br /&gt;
从上面的图可以注意到：哈希冲突的节点在链表中是插入到链表尾部的&lt;br /&gt;
&lt;br /&gt;
在Java8之前是插入到前面的，但是Java8改成插入到尾部了，这样做的原因（据说）是因为扩容时会改变链表的顺序，在多线程条件下会导致形成闭环（从而可能引起死循环）。&lt;br /&gt;
&lt;br /&gt;
= fail-fast机制=&lt;br /&gt;
在HashMap中存在一个变量记录修改的次数`modCount`,当这个次数和期待的不一致的时候就会抛出`ConcurrentModificationException`。这种机制被称之为&amp;quot;Fail-Fast”，意味着出现错误的时候尽早结束。通常在`java.util`下面的迭代器都是这类的，如果在迭代的中途数据被其他线程修改了，那么就会（尽可能的，当然并不能保证）触发这个检测。&lt;br /&gt;
&lt;br /&gt;
而`java.util.concurrent`包下的迭代器是&amp;quot;Fail-Safe&amp;quot;的，例如ConcurrentHashMap、CopyOnWriteArrayList等。&lt;br /&gt;
&lt;br /&gt;
= 性能分析=&lt;br /&gt;
HashMap对于`get`和`put`操作的复杂度是常数级 $\displaystyle{O(1)}$ ，在最坏的情况下，因为使用了红黑树进行查找，复杂度为 $\displaystyle{O(log(n))}$ 。&lt;br /&gt;
&lt;br /&gt;
* [https://stackoverflow.com/questions/20448477/cant-understand-poisson-part-of-hash-tables-from-sun-documentation Can&amp;#039;t understand Poisson part of Hash tables from Sun documentation]&lt;br /&gt;
* [https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap What is the significance of load factor in HashMap?]&lt;br /&gt;
* [http://rabbit.eng.miami.edu/class/een318/poisson.pdf Testing a Hash Function using Probability]&lt;/div&gt;</summary>
		<author><name>imported&gt;Riguz</name></author>
	</entry>
</feed>