Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
Coudy Hou
JavaGuide
提交
39388a4a
J
JavaGuide
项目概览
Coudy Hou
/
JavaGuide
与 Fork 源项目一致
从无法访问的项目Fork
通知
5
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
JavaGuide
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
39388a4a
编写于
2月 25, 2021
作者:
G
guide
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Update HashMap(JDK1.8)源码+底层数据结构分析.md
上级
11ebd35f
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
95 addition
and
68 deletion
+95
-68
docs/java/collection/HashMap(JDK1.8)源码+底层数据结构分析.md
docs/java/collection/HashMap(JDK1.8)源码+底层数据结构分析.md
+95
-68
未找到文件。
docs/java/collection/HashMap(JDK1.8)源码+底层数据结构分析.md
浏览文件 @
39388a4a
<!-- MarkdownTOC -->
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
-
[
HashMap 简介
](
#hashmap-简介
)
-
[
底层数据结构分析
](
#底层数据结构分析
)
-
[
JDK1.8
之前
](
#jdk18
之前
)
-
[
JDK1.8
之后
](
#jdk18
之后
)
-
[
HashMap
源码分析
](
#hashmap
源码分析
)
-
[
JDK1.8
之前
](
#jdk18-
之前
)
-
[
JDK1.8
之后
](
#jdk18-
之后
)
-
[
HashMap
源码分析
](
#hashmap-
源码分析
)
-
[
构造方法
](
#构造方法
)
-
[
put方法
](
#put方法
)
-
[
get方法
](
#get方法
)
-
[
resize方法
](
#resize方法
)
-
[
HashMap常用方法测试
](
#hashmap常用方法测试
)
-
[
put 方法
](
#put-方法
)
-
[
get 方法
](
#get-方法
)
-
[
resize 方法
](
#resize-方法
)
-
[
HashMap 常用方法测试
](
#hashmap-常用方法测试
)
<!-- /code_chunk_output -->
<!-- /MarkdownTOC -->
> 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献!
## HashMap 简介
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8 之后 HashMap 的组成多了红黑树,在满足下面两个条件之后,会执行链表转红黑树操作,以此来加快搜索速度。
-
链表长度大于阈值(默认为 8)
-
HashMap
数组长度超过64å
-
HashMap
数组长度超过 64
## 底层数据结构分析
### JDK1.8之前
### JDK1.8 之前
JDK1.8 之前 HashMap 底层是
**数组和链表**
结合在一起使用也就是
**链表散列**
。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash
值,然后通过
`(n - 1) & hash`
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过
`(n - 1) & hash`
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
**JDK 1.8 HashMap 的 hash 方法源码:**
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
JDK 1.8 的 hash
方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
```
java
static
final
int
hash
(
Object
key
)
{
int
h
;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return
(
key
==
null
)
?
0
:
(
h
=
key
.
hashCode
())
^
(
h
>>>
16
);
}
```
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
```
java
static
final
int
hash
(
Object
key
)
{
int
h
;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return
(
key
==
null
)
?
0
:
(
h
=
key
.
hashCode
())
^
(
h
>>>
16
);
}
```
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
```
java
static
int
hash
(
int
h
)
{
...
...
@@ -65,57 +73,60 @@ static int hash(int h) {
![
jdk1.8之前的内部结构
](
https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/jdk1.8之前的内部结构.png
)
### JDK1.8之后
### JDK1.8 之后
相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。
当链表长度大于阈值(默认为 8)时,会首先调用
`treeifyBin()`
方法,这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行
`resize()`
方法对数组扩容。相关源码这里就不贴了,重点关注
`treeifyBin()`
方法即可!
当链表长度大于阈值(默认为 8)时,会首先调用
`treeifyBin()`
方法,这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行
`resize()`
方法对数组扩容。相关源码这里就不贴了,重点关注
`treeifyBin()`
方法即可!
![](
https://oscimg.oschina.net/oscnet/up-bba283228693dae74e78da1ef7a9a04c684.png
)
**类的属性:**
```
java
public
class
HashMap
<
K
,
V
>
extends
AbstractMap
<
K
,
V
>
implements
Map
<
K
,
V
>,
Cloneable
,
Serializable
{
// 序列号
private
static
final
long
serialVersionUID
=
362498820763181265L
;
private
static
final
long
serialVersionUID
=
362498820763181265L
;
// 默认的初始容量是16
static
final
int
DEFAULT_INITIAL_CAPACITY
=
1
<<
4
;
static
final
int
DEFAULT_INITIAL_CAPACITY
=
1
<<
4
;
// 最大容量
static
final
int
MAXIMUM_CAPACITY
=
1
<<
30
;
static
final
int
MAXIMUM_CAPACITY
=
1
<<
30
;
// 默认的填充因子
static
final
float
DEFAULT_LOAD_FACTOR
=
0.75f
;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static
final
int
TREEIFY_THRESHOLD
=
8
;
static
final
int
TREEIFY_THRESHOLD
=
8
;
// 当桶(bucket)上的结点数小于这个值时树转链表
static
final
int
UNTREEIFY_THRESHOLD
=
6
;
// 桶中结构转化为红黑树对应的table的最小大小
static
final
int
MIN_TREEIFY_CAPACITY
=
64
;
// 存储元素的数组,总是2的幂次倍
transient
Node
<
k
,
v
>[]
table
;
transient
Node
<
k
,
v
>[]
table
;
// 存放具体元素的集
transient
Set
<
map
.
entry
<
k
,
v
>>
entrySet
;
// 存放元素的个数,注意这个不等于数组的长度。
transient
int
size
;
// 每次扩容和更改map结构的计数器
transient
int
modCount
;
transient
int
modCount
;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int
threshold
;
// 加载因子
final
float
loadFactor
;
}
```
-
**loadFactor加载因子**
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
-
**loadFactor 加载因子**
loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
**loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**
。
**loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值**
。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16
*
0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16
\*
0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
-
**threshold**
**threshold = capacity
* loadFactor**
,
**当
Size>=threshold**
的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是
**衡量数组是否需要扩增的一个标准**
。
**threshold = capacity
\* loadFactor**
,
**当
Size>=threshold**
的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是
**衡量数组是否需要扩增的一个标准**
。
**Node节点类源码:**
**Node
节点类源码:**
```
java
// 继承自 Map.Entry<K,V>
...
...
@@ -158,7 +169,9 @@ static class Node<K,V> implements Map.Entry<K,V> {
}
}
```
**树节点类源码:**
```
java
static
final
class
TreeNode
<
K
,
V
>
extends
LinkedHashMap
.
Entry
<
K
,
V
>
{
TreeNode
<
K
,
V
>
parent
;
// 父
...
...
@@ -177,7 +190,9 @@ static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
r
=
p
;
}
```
## HashMap源码分析
## HashMap 源码分析
### 构造方法
HashMap 中有四个构造方法,它们分别如下:
...
...
@@ -187,18 +202,18 @@ HashMap 中有四个构造方法,它们分别如下:
public
HashMap
()
{
this
.
loadFactor
=
DEFAULT_LOAD_FACTOR
;
// all other fields defaulted
}
// 包含另一个“Map”的构造函数
public
HashMap
(
Map
<?
extends
K
,
?
extends
V
>
m
)
{
this
.
loadFactor
=
DEFAULT_LOAD_FACTOR
;
putMapEntries
(
m
,
false
);
//下面会分析到这个方法
}
// 指定“容量大小”的构造函数
public
HashMap
(
int
initialCapacity
)
{
this
(
initialCapacity
,
DEFAULT_LOAD_FACTOR
);
}
// 指定“容量大小”和“加载因子”的构造函数
public
HashMap
(
int
initialCapacity
,
float
loadFactor
)
{
if
(
initialCapacity
<
0
)
...
...
@@ -212,7 +227,7 @@ HashMap 中有四个构造方法,它们分别如下:
}
```
**putMapEntries方法:**
**putMapEntries
方法:**
```
java
final
void
putMapEntries
(
Map
<?
extends
K
,
?
extends
V
>
m
,
boolean
evict
)
{
...
...
@@ -240,17 +255,22 @@ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
}
}
```
### put方法
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
**对putVal方法添加元素的分析如下:**
### put 方法
HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。
**对 putVal 方法添加元素的分析如下:**
1.
如果定位到的数组位置没有元素 就直接插入。
2.
如果定位到的数组位置有元素就和要插入的
key比较,如果key相同就直接覆盖,如果key不相同,就判断p
是否是一个树节点,如果是就调用
`e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)`
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
2.
如果定位到的数组位置有元素就和要插入的
key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p
是否是一个树节点,如果是就调用
`e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)`
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
ps:下图有一个小问题,来自
[
issue#608
](
https://github.com/Snailclimb/JavaGuide/issues/608
)
指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。
说明:下图有两个小问题:
![
put方法
](
https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png
)
-
[
issue#608
](
https://github.com/Snailclimb/JavaGuide/issues/608
)
指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。
-
当链表长度大于阈值(默认为 8)并且 HashMap 数组长度超过 64 的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。参考 HashMap 的
`treeifyBin()`
方法
![
](
https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png
)
```
java
public
V
put
(
K
key
,
V
value
)
{
...
...
@@ -302,7 +322,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if
(
e
!=
null
)
{
if
(
e
!=
null
)
{
// 记录e的value
V
oldValue
=
e
.
value
;
// onlyIfAbsent为false或者旧值为null
...
...
@@ -323,21 +343,21 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
// 插入后回调
afterNodeInsertion
(
evict
);
return
null
;
}
}
```
**我们再来对比一下 JDK1.7 put方法的代码**
**我们再来对比一下 JDK1.7 put
方法的代码**
**对于
put
方法的分析如下:**
**对于
put
方法的分析如下:**
-
①如果定位到的数组位置没有元素 就直接插入。
-
②
如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key
相同就直接覆盖,不同就采用头插法插入元素。
-
①
如果定位到的数组位置没有元素 就直接插入。
-
②
如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key
相同就直接覆盖,不同就采用头插法插入元素。
```
java
public
V
put
(
K
key
,
V
value
)
if
(
table
==
EMPTY_TABLE
)
{
inflateTable
(
threshold
);
}
if
(
table
==
EMPTY_TABLE
)
{
inflateTable
(
threshold
);
}
if
(
key
==
null
)
return
putForNullKey
(
value
);
int
hash
=
hash
(
key
);
...
...
@@ -348,7 +368,7 @@ public V put(K key, V value)
V
oldValue
=
e
.
value
;
e
.
value
=
value
;
e
.
recordAccess
(
this
);
return
oldValue
;
return
oldValue
;
}
}
...
...
@@ -358,7 +378,8 @@ public V put(K key, V value)
}
```
### get方法
### get 方法
```
java
public
V
get
(
Object
key
)
{
Node
<
K
,
V
>
e
;
...
...
@@ -389,8 +410,11 @@ final Node<K,V> getNode(int hash, Object key) {
return
null
;
}
```
### resize方法
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
### resize 方法
进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。
```
java
final
Node
<
K
,
V
>[]
resize
()
{
Node
<
K
,
V
>[]
oldTab
=
table
;
...
...
@@ -409,7 +433,7 @@ final Node<K,V>[] resize() {
}
else
if
(
oldThr
>
0
)
// initial capacity was placed in threshold
newCap
=
oldThr
;
else
{
else
{
// signifies using defaults
newCap
=
DEFAULT_INITIAL_CAPACITY
;
newThr
=
(
int
)(
DEFAULT_LOAD_FACTOR
*
DEFAULT_INITIAL_CAPACITY
);
...
...
@@ -433,7 +457,7 @@ final Node<K,V>[] resize() {
newTab
[
e
.
hash
&
(
newCap
-
1
)]
=
e
;
else
if
(
e
instanceof
TreeNode
)
((
TreeNode
<
K
,
V
>)
e
).
split
(
this
,
newTab
,
j
,
oldCap
);
else
{
else
{
Node
<
K
,
V
>
loHead
=
null
,
loTail
=
null
;
Node
<
K
,
V
>
hiHead
=
null
,
hiTail
=
null
;
Node
<
K
,
V
>
next
;
...
...
@@ -473,7 +497,9 @@ final Node<K,V>[] resize() {
return
newTab
;
}
```
## HashMap常用方法测试
## HashMap 常用方法测试
```
java
package
map
;
...
...
@@ -530,7 +556,7 @@ public class HashMapDemo {
for
(
java
.
util
.
Map
.
Entry
<
String
,
String
>
entry
:
entrys
)
{
System
.
out
.
println
(
entry
.
getKey
()
+
"--"
+
entry
.
getValue
());
}
/**
* HashMap其他常用方法
*/
...
...
@@ -547,4 +573,4 @@ public class HashMapDemo {
}
```
```
\ No newline at end of file
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录