Skip to content

Commit d2dd28b

Browse files
author
hollis.zhl
committed
hashmap 相关知识完善
1 parent 7cd6106 commit d2dd28b

File tree

10 files changed

+1287
-8
lines changed

10 files changed

+1287
-8
lines changed

docs/_sidebar.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757

5858
* [Integer的缓存机制](/basics/java-basic/integer-cache.md)
5959

60-
* [如何正确定义接口的返回值(boolean/Boolean)类型及命名(success/Success)](/basics/java-basic/success-isSuccess-and-bollean-Bollean.md)
60+
* [如何正确定义接口的返回值(boolean/Boolean)类型及命名(success/isSuccess)](/basics/java-basic/success-isSuccess-and-bollean-Bollean.md)
6161

6262
* String
6363

@@ -115,7 +115,15 @@
115115

116116
* Java 8中Map相关的红黑树的引用背景、原理等
117117

118-
* HashMap的容量、扩容、hash等原理
118+
* [HashMap的容量、扩容](/basics/java-basic/hashmap-capacity.md)
119+
120+
* [HashMap中hash方法的原理](/basics/java-basic/hash-in-hashmap.md)
121+
122+
* [为什么HashMap的默认容量设置成16](/basics/java-basic/hashmap-default-capacity.md)
123+
124+
* [为什么HashMap的默认负载因子设置成0.75](/basics/java-basic/hashmap-default-loadfactor.md)
125+
126+
* [为什么建议设置HashMap的初始容量,设置多少合适](/basics/java-basic/hashmap-init-capacity.md)
119127

120128
* [Java 8中stream相关用法](/basics/java-basic/stream.md)
121129

@@ -259,9 +267,9 @@
259267

260268
* junit 和Spring 的结合
261269

262-
* mock
270+
* [mock](/basics/java-basic/mock.md)
263271

264-
* mockito
272+
* [mockito](/basics/java-basic/ut-with-mockito.md)
265273

266274
* 内存数据库(h2)
267275

@@ -531,7 +539,7 @@
531539

532540
* volatile和有序性
533541

534-
* 有了symchronized为什么还需要volatile
542+
* 有了synchronized为什么还需要volatile
535543

536544
* 线程相关方法
537545

docs/basics/java-basic/hash-in-hashmap.md

Lines changed: 255 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
很多人在通过阅读源码的方式学习Java,这是个很好的方式。而JDK的源码自然是首选。在JDK的众多类中,我觉得HashMap及其相关的类是设计的比较好的。很多人读过HashMap的代码,不知道你们有没有和我一样,觉得HashMap中关于容量相关的参数定义的太多了,傻傻分不清楚。
2+
3+
其实,这篇文章介绍的内容比较简单,只要认真的看看HashMap的原理还是可以理解的,单独写一篇文章的原因是因为我后面还有几篇关于HashMap源码分析的文章,这些概念不熟悉的话阅读后面的文章会很吃力。
4+
5+
先来看一下,HashMap中都定义了哪些成员变量。
6+
7+
[<img src="http://www.hollischuang.com/wp-content/uploads/2018/05/paramInMap.png" alt="paramInMap" width="523" height="288" class="aligncenter size-full wp-image-2424" />][1]
8+
9+
上面是一张HashMap中主要的成员变量的图,其中有一个是我们本文主要关注的: `size``loadFactor``threshold``DEFAULT_LOAD_FACTOR``DEFAULT_INITIAL_CAPACITY`
10+
11+
我们先来简单解释一下这些参数的含义,然后再分析他们的作用。
12+
13+
HashMap类中有以下主要成员变量:
14+
15+
* transient int size;
16+
* 记录了Map中KV对的个数
17+
* loadFactor
18+
* 装载印子,用来衡量HashMap满的程度。loadFactor的默认值为0.75f(`static final float DEFAULT_LOAD_FACTOR = 0.75f;`)。
19+
* int threshold;
20+
* 临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
21+
* 除了以上这些重要成员变量外,HashMap中还有一个和他们紧密相关的概念:capacity
22+
* 容量,如果不指定,默认容量是16(`static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;`)
23+
24+
可能看完了你还是有点蒙,size和capacity之间有啥关系?为啥要定义这两个变量。loadFactor和threshold又是干啥的?
25+
26+
### size 和 capacity
27+
28+
HashMap中的size和capacity之间的区别其实解释起来也挺简单的。我们知道,HashMap就像一个“桶”,那么capacity就是这个桶“当前”最多可以装多少元素,而size表示这个桶已经装了多少元素。来看下以下代码:
29+
30+
Map<String, String> map = new HashMap<String, String>();
31+
map.put("hollis", "hollischuang");
32+
33+
Class<?> mapType = map.getClass();
34+
Method capacity = mapType.getDeclaredMethod("capacity");
35+
capacity.setAccessible(true);
36+
System.out.println("capacity : " + capacity.invoke(map));
37+
38+
Field size = mapType.getDeclaredField("size");
39+
size.setAccessible(true);
40+
System.out.println("size : " + size.get(map));
41+
42+
43+
我们定义了一个新的HashMap,并想其中put了一个元素,然后通过反射的方式打印capacity和size。输出结果为:**capacity : 16、size : 1**
44+
45+
默认情况下,一个HashMap的容量(capacity)是16,设计成16的好处我在《[全网把Map中的hash()分析的最透彻的文章,别无二家。][2]》中也简单介绍过,主要是可以使用按位与替代取模来提升hash的效率。
46+
47+
为什么我刚刚说capacity就是这个桶“当前”最多可以装多少元素呢?当前怎么理解呢。其实,HashMap是具有扩容机制的。在一个HashMap第一次初始化的时候,默认情况下他的容量是16,当达到扩容条件的时候,就需要进行扩容了,会从16扩容成32。
48+
49+
我们知道,HashMap的重载的构造函数中,有一个是支持传入initialCapacity的,那么我们尝试着设置一下,看结果如何。
50+
51+
Map<String, String> map = new HashMap<String, String>(1);
52+
53+
Class<?> mapType = map.getClass();
54+
Method capacity = mapType.getDeclaredMethod("capacity");
55+
capacity.setAccessible(true);
56+
System.out.println("capacity : " + capacity.invoke(map));
57+
58+
Map<String, String> map = new HashMap<String, String>(7);
59+
60+
Class<?> mapType = map.getClass();
61+
Method capacity = mapType.getDeclaredMethod("capacity");
62+
capacity.setAccessible(true);
63+
System.out.println("capacity : " + capacity.invoke(map));
64+
65+
66+
Map<String, String> map = new HashMap<String, String>(9);
67+
68+
Class<?> mapType = map.getClass();
69+
Method capacity = mapType.getDeclaredMethod("capacity");
70+
capacity.setAccessible(true);
71+
System.out.println("capacity : " + capacity.invoke(map));
72+
73+
74+
分别执行以上3段代码,分别输出:**capacity : 2、capacity : 8、capacity : 16**
75+
76+
也就是说,默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(1->1、7->8、9->16)
77+
78+
> 这里有一个小建议:在初始化HashMap的时候,应该尽量指定其大小。尤其是当你已知map中存放的元素个数时。(《阿里巴巴Java开发规约》)
79+
80+
### loadFactor 和 threshold
81+
82+
前面我们提到过,HashMap有扩容机制,就是当达到扩容条件时会进行扩容,从16扩容到32、64、128...
83+
84+
那么,这个扩容条件指的是什么呢?
85+
86+
其实,HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。
87+
88+
在HashMap中,threshold = loadFactor * capacity。
89+
90+
loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数。
91+
92+
对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。
93+
94+
验证代码如下:
95+
96+
Map<String, String> map = new HashMap<>();
97+
map.put("hollis1", "hollischuang");
98+
map.put("hollis2", "hollischuang");
99+
map.put("hollis3", "hollischuang");
100+
map.put("hollis4", "hollischuang");
101+
map.put("hollis5", "hollischuang");
102+
map.put("hollis6", "hollischuang");
103+
map.put("hollis7", "hollischuang");
104+
map.put("hollis8", "hollischuang");
105+
map.put("hollis9", "hollischuang");
106+
map.put("hollis10", "hollischuang");
107+
map.put("hollis11", "hollischuang");
108+
map.put("hollis12", "hollischuang");
109+
Class<?> mapType = map.getClass();
110+
111+
Method capacity = mapType.getDeclaredMethod("capacity");
112+
capacity.setAccessible(true);
113+
System.out.println("capacity : " + capacity.invoke(map));
114+
115+
Field size = mapType.getDeclaredField("size");
116+
size.setAccessible(true);
117+
System.out.println("size : " + size.get(map));
118+
119+
Field threshold = mapType.getDeclaredField("threshold");
120+
threshold.setAccessible(true);
121+
System.out.println("threshold : " + threshold.get(map));
122+
123+
Field loadFactor = mapType.getDeclaredField("loadFactor");
124+
loadFactor.setAccessible(true);
125+
System.out.println("loadFactor : " + loadFactor.get(map));
126+
127+
map.put("hollis13", "hollischuang");
128+
Method capacity = mapType.getDeclaredMethod("capacity");
129+
capacity.setAccessible(true);
130+
System.out.println("capacity : " + capacity.invoke(map));
131+
132+
Field size = mapType.getDeclaredField("size");
133+
size.setAccessible(true);
134+
System.out.println("size : " + size.get(map));
135+
136+
Field threshold = mapType.getDeclaredField("threshold");
137+
threshold.setAccessible(true);
138+
System.out.println("threshold : " + threshold.get(map));
139+
140+
Field loadFactor = mapType.getDeclaredField("loadFactor");
141+
loadFactor.setAccessible(true);
142+
System.out.println("loadFactor : " + loadFactor.get(map));
143+
144+
145+
输出结果:
146+
147+
capacity : 16
148+
size : 12
149+
threshold : 12
150+
loadFactor : 0.75
151+
152+
capacity : 32
153+
size : 13
154+
threshold : 24
155+
loadFactor : 0.75
156+
157+
158+
当HashMap中的元素个数达到13的时候,capacity就从16扩容到32了。
159+
160+
HashMap中还提供了一个支持传入initialCapacity,loadFactor两个参数的方法,来初始化容量和装载因子。不过,一般不建议修改loadFactor的值。
161+
162+
### 总结
163+
164+
HashMap中size表示当前共有多少个KV对,capacity表示当前HashMap的容量是多少,默认值是16,每次扩容都是成倍的。loadFactor是装载因子,当Map中元素个数超过`loadFactor* capacity`的值时,会触发扩容。`loadFactor* capacity`可以用threshold表示。
165+
166+
PS:文中分析基于JDK1.8.0_73
167+
168+
[1]: http://www.hollischuang.com/wp-content/uploads/2018/05/paramInMap.png
169+
[2]: http://www.hollischuang.com/archives/2091

0 commit comments

Comments
 (0)