HashMap设计原理和实现分析Word格式.docx
《HashMap设计原理和实现分析Word格式.docx》由会员分享,可在线阅读,更多相关《HashMap设计原理和实现分析Word格式.docx(19页珍藏版)》请在冰豆网上搜索。
其结构如下图所示:
HashMap内部组织结构由上图所示,现在来看一下HashMap的基本工作流程:
2.什么是阀值?
HashMap设计的初衷,是为了尽可能地迅速根据Key的hashCode值,直接就可以定位到对应的Entry<
对象,然后得到Value。
请读者考虑这样一个问题:
当我们使用
HashMapmap=newHashMap()语句时,我们会创建一个HashMap对象,它内部的
Entry[]table的大小为
16,我们假定Entry[]table的大小会改变。
现在,我们现在向它添加160对Key值完全不同的键值对<
,那么,该HashMap内部有可能下面这种情况:
即对于每一个桶中的由Entry<
组成的链表的长度会非常地长!
我们知道,对于查找链表操作的时间复杂度是很高的,为O(n)。
这样的一个HashMap的性能会很低很低,如下图所示:
现在再来分析一下这个问题,当前的HashMap能够实现:
1.根据Key的hashCode,可以直接定位到存储这个Entry<
的桶所在的位置,这个时间的复杂度为O
(1);
2.在桶中查找对应的Entry<
对象节点,需要遍历这个桶的Entry<
链表,时间复杂度为O(n);
那么,现在,我们应该尽可能地将第2个问题的时间复杂度o(n)降到最低,读者现在是不是有想法了:
我们应该要求桶中的链表的长度越短越好!
桶中链表的长度越短,所消耗的查找时间就越低,最好就是一个桶中就一个Entry<
对象节点就好了!
这样一来,桶中的Entry<
对象节点要求尽可能第少,这就要求,HashMap中的桶的数量要多了。
我们知道,HashMap的桶数目,即Entry[]table数组的长度,由于数组是内存中连续的存储单元,它的空间代价是很大的,但是它的随机存取的速度是Java集合中最快的。
我们增大桶的数量,而减少Entry<
链表的长度,来提高从HashMap中读取数据的速度。
这是典型的拿空间换时间的策略。
但是我们不能刚开始就给HashMap分配过多的桶(即Entry[]table数组起始不能太大),这是因为数组是连续的内存空间,它的创建代价很大,况且我们不能确定给HashMap分配这么大的空间,它实际到底能够用多少,为了解决这一个问题,HashMap采用了根据实际的情况,动态地分配桶的数量。
HashMap的权衡策略
要动态分配桶的数量,这就要求要有一个权衡的策略了,HashMap的权衡策略是这样的:
如果
HashMap的大小>
HashMap的容量(即Entry[]table的大小)*加载因子(经验值0.75)
则
HashMap中的Entry[]table的容量扩充为当前的一倍;
然后重新将以前桶中的Entry<
链表重新分配到各个桶中
上述的
HashMap的容量(即Entry[]table的大小)*加载因子(经验值0.75)就是所谓的阀值(threshold):
最后,请读者看一个实例:
默认创建的HashMapmap=newHashMap();
map的容量是
16,那么,当我们往
map中添加第几个完全不同的键值对<
时,HashMap的容量会扩充呢?
呵呵,很简单的计算:
由于默认的加载因子是0.75
,那么,此时map的阀值是
16*0.75=12,即添加第13
个键值对<
的时候,map的容量会扩充一倍。
这时候读者可能会有疑问:
本来Entry[]table的容量是16,当放入12个键值对<
后,不是至少还剩下4个Entry[]table
元素没有被使用到吗?
这不是浪费了宝贵的空间了吗?
!
确实如此,但是为了尽可能第减少桶中的Entry<
链表的长度,以提高HashMap的存取性能,确定的这个经验值。
如果读者你对存取效率要求的不是太高,想省点空间的话,你可以newHashMap(intinitialCapacity,floatloadFactor)构造方法将这个因子设置得大一些也无妨。
2.HashMap的算法实现解析
HashMap的算法实现最重要的两个是put()和get()两个方法,下面我将分析这两个方法:
[java]
viewplain
copy
print?
1.public
V
put(K
key,
value);
2.public
get(Object
key);
另外,HashMap支持Key值为null的情况,我也将详细地讨论这个问题。
1.向HashMap中存储一对键值对<
流程---put()方法实现:
put()方法-向HashMap存储键值对<
a.
获取这个Key的hashcode值,根据此值确定应该将这一对键值对存放在哪一个桶中,即确定要存放桶的索引;
b.
遍历所在桶中的Entry<
链表,查找其中是否已经有了以Key值为Key存储的Entry<
对象,
c1.若已存在,定位到对应的Entry<
其中的Value值更新为新的Value值;
返回旧值;
c2.若不存在,则根据键值对<
创建一个新的Entry<
对象,然后添加到这个桶的Entry<
链表的头部。
d.
当前的HashMap的大小(即Entry<
key,Value>
节点的数目)是否超过了阀值,若超过了阀值(threshold),则增大HashMap的容量(即Entry[]table的大小),并且重新组织内部各个Entry<
排列。
详细流程如下列的代码所示:
1./**
2.
*
将<
键值对存到HashMap中,如果Key在HashMap中已经存在,那么最终返回被替换掉的Value值。
3.
Key
和Value允许为空
4.
*/
5.public
value)
{
6.
7.
//1.如果key为null,那么将此value放置到table[0],即第一个桶中
8.
if
(key
==
null)
9.
return
putForNullKey(value);
10.
//2.重新计算hashcode值,
11.
int
hash
=
hash(key.hashCode());
12.
//3.计算当前hashcode值应当被分配到哪一个桶中,获取桶的索引
13.
i
indexFor(hash,
table.length);
14.
//4.循环遍历该桶中的Entry列表
15.
for
(Entry<
e
table[i];
!
null;
e.next)
16.
Object
k;
17.
//5.
查找Entry<
链表中是否已经有了以Key值为Key存储的Entry<
对象,
18.
//已经存在,则将Value值覆盖到对应的Entry<
对象节点上
19.
(e.hash
&
((k
e.key)
key
||
key.equals(k)))
{//请读者注意这个判定条件,非常重要!
20.
oldValue
e.value;
21.
e.value
value;
22.
e.recordAccess(this);
23.
oldValue;
24.
}
25.
26.
modCount++;
27.
//6不存在,则根据键值对<
创建一个新的Entry<
28.
addEntry(hash,
value,
i);
29.
30.}
31.
32./**
33.
为null,则将Entry<
null,Value>
放置到第一桶table[0]中
34.
35.private
putForNullKey(V
36.
table[0];
37.
(e.key
38.
39.
40.
41.
42.
43.
44.
45.
addEntry(0,
null,
0);
46.
47.}
根据特定的hashcode
重新计算hash值,
由于JVM生成的的hashcode的低字节(lower
bits)冲突概率大,(JDK只是这么一说,至于为什么我也不清楚)
为了提高性能,HashMap对Key的hashcode再加工,取Key的hashcode的高字节参与运算
5.
6.static
hash(int
h)
//
This
function
ensures
that
hashCodes
differ
only
by
constant
multiples
at
each
bit
position
have
a
bounded
number
of
collisions
(approximately
8
default
load
factor).
h
^=
(h
>
20)
^
12);
7)
4);
12.}
14./**
返回此hashcode应当分配到的桶的索引
17.static
indexFor(int
h,
length)
(length-1);
19.}
当HashMap的大小大于阀值时,HashMap容量的扩充算法当当前的HashMap的大小大于阀值时,HashMap会对此HashMap的容量进行扩充,即对内部的Entry[]table数组进行扩充。
HashMap对容量(Entry[]table数组长度)有两点要求:
1.容量的大小应当是2的N次幂;
2.当容量大小超过阀值时,容量扩充为当前的一倍;
这里第2点很重要,如果当前的HashMap的容量为16,需要扩充时,容量就要变成16*2=32,接着就是32*2=64、64*2=128、128*2=256.........可以看出,容量扩充的大小是呈指数级的级别递增的。
这里容量扩充的操作可以分为以下几个步骤:
1.申请一个新的、大小为当前容量两倍的数组;
2.
将旧数组的Entry[]table中的链表重新计算hash值,然后重新均匀地放置到新的扩充数组中;
3.
释放旧的数组;
由上述的容量扩充的步骤来看,一次容量扩充的代价非常大,所以在容量扩充时,扩充的比例为当前的一倍,这样做是尽量减少容量扩充的次数。
为了提高HashMap的性能:
1.在使用HashMap的过程中,你比较明确它要容纳多少Entry<
,你应该在创建HashMap的时候直接指定它的容量;
2.如果你确定HashMap的使用的过程中,大小会非常大,那么你应该控制好加载因子的大小,尽量将它设置得大些。
避免Entry[]table过大,而利用率觉很低。
Rehashes
the
contents
this
map
into
new
array
with
larger
capacity.
method
is
called
automatically
when
keys
in
reaches
its
threshold.
If
current
capacity
MAXIMUM_CAPACITY,
does
not
resize
map,
but
sets
threshold
to
Integer.MAX_VALUE.
has
effect
preventing
future
calls.
@param
newCapacity
capacity,
MUST
be
power
two;
must
greater
than
unless
MAXIMUM_CAPACITY
(in
which
case
value
irrelevant).
15.void
resize(int
newCapacity)
Entry[]
oldTable
table;
oldCapacity
oldTable.length;
(oldCapacity
MAXIMUM_CAPACITY)
Integer.MAX_VALUE;
return;
newTable
Entry[newCapacity];
transfer(newTable);
table
newTable;
(int)(newCapacity
loadFactor);
27.}
29./**
30.
Transfers
all
entries
from
newTable.
32.void
transfer(Entry[]
newTable)
src
newTable.length;
35.
(int
j
0;
<
src.length;
j++)
Entry<
src[j];
(e
src[j]
do
next
e.next;
indexFor(e.hash,
newCapacity);
e.next
newTable[i];
newTable[i]
e;
next;
while
null);
47.
48.}
为什么JDK建议我们重写Object.equals(Objectobj)方法时,需要保证对象可以返回相同的hashcode值?
Java程序员都看过JDK的API文档,该文档关于Object.equals(Objectobj)方法,有这样的描述:
“注意:
当此方法被重写时,通常有必要重写hashCode
方法,以维护hashCode
方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
”
读者虽然知道这个协定,但是不一定真正知道为什么会有这一个要求,现在,就来看看原因吧。
请读者再注意看一下上述的额put()方法实现,当遍历某个桶中的Entry<
链表来查找Entry实例的过程中所使用的判断条件:
1.for
对于给定的Key,Value,判断该Key是否与Entry链表中有某一个Entry对象的Key值相等