[TOC]
Java-手写HashMap
1 | public class MyHashMap{ |
我将顺着下面的这些问题,一步步完成MyHashMap
的实现。(没有实现红黑树)
一、HashMap 为什么选择数组+链表作为底层数据结构?
- 数组的查询复杂度是 O(1),但插入效率(尤其是中间和头部插入)较低。
- 链表的查询复杂度是 O(n),但插入复杂度为 O(1)。
- 结合数组和链表的优点,提高了查询和插入效率。
1 | package myHashMap; |
二、数组的大小怎么规定?
源码中规定,数组的大小必须等于2^n
,这是因为:
如果 table 的长度是 2^n,元素在数组中的索引计算
index = hash % table.length
可等效为index = hash & (table.length - 1)
,用位运算替代取模运算,提高计算效率。当数组扩容时,如果元素原本分布均匀,扩容后也能保持均匀分布。例如:
假设某元素的哈希值为
10101100
旧数组计算索引:
1
2
3makefile复制编辑hash = 10101100
length - 1 = 00000111
index = 00000100 (4)新数组计算索引:
1
2
3makefile复制编辑hash = 10101100
length - 1 = 00001111
index = 00001100 (12)观察第四位(从右数):
- 高位为 0:位置不变。
- 高位为 1:移动到新位置(原索引位置 + 原容量)。
源码中,初始化的数组大小为16。
1 | package myHashMap; |
三、数组什么时候进行扩容?
我们定义一个size
来存储【数组当前实际存储的元素个数】,然后有两个问题:
1)什么时候进行扩容?
2)扩容扩到多少?
对于问题一,HashMap的源码中,是当size=table.length*0.75
时,就进行扩容。
- 我们将
0.75
称为负载因子,定义LOAD_FACTOR
; table.length*LOAD_FACTOR
就称为阈值,定义threshold
。
对于问题二,HashMap的源码中是将数组大小翻倍,这样保证扩容后的数组长度依然是2^n
。
1 | package myHashMap; |
四、元素的 hash 值是直接返回 hashCode() 吗?
若
hash = obj.hashCode()
,索引计算index = hash & (length - 1)
仅受 hashCode() 低位影响,高位不起作用,容易导致冲突。为了让高位也参与计算,我们这样计算 hash 值:
1
2hash = (hashCode) ^ (hashCode >>> 16)
index = hash & (length - 1)
1 | package myHashMap; |
五、HashMap 的 put()、get()、resize() 方法
put()
方法:
- table 是否已初始化,若未初始化则先进行初始化。
- 是否超过阈值,若超出则先扩容
resize()
。 - 计算在数组中的索引值。
- 若索引位置为空,直接插入。
- 若索引位置不为空,则遍历链表:
- 若 key 相同,直接覆盖。这里 key 相同指的是:内存地址相同;或者,如果内存地址不同,那么通过
equals()
判断值是否相同; - 若 key 不同且已遍历到链表尾部,则新增节点插入链表尾部。
- 若 key 相同,直接覆盖。这里 key 相同指的是:内存地址相同;或者,如果内存地址不同,那么通过
get()
与resize()
方法流程不再叙述,resize()
就是按创建一个更长(翻倍)的数组,然后进行复制。
直接看代码吧!
六、最终版本
1 | package myHashMap; |
六、为什么 Java 8 更新,链表长度达到 8 时转换为红黑树?
当链表过长时,由于链表的查询复杂度是O(n)
,会导致查询效率降低;所以用时间复杂度为O(log n)
的红黑树进行优化。
但事实上,根据泊松分布公式,出现8个 hash 冲突的几率非常低;所以一旦出现8个以上 hash 冲突,多半是误操作或者漏洞攻击。
之前就有利用 HashMap 这种链表结构攻击,导致服务器崩溃,所以这个变红黑树其实是补漏洞。正常操作很难出现红黑树。