Map 综述一彻头彻尾理解 HashMap文档格式.docx
《Map 综述一彻头彻尾理解 HashMap文档格式.docx》由会员分享,可在线阅读,更多相关《Map 综述一彻头彻尾理解 HashMap文档格式.docx(18页珍藏版)》请在冰豆网上搜索。
,Cloneable,Serializable{
...
}
三.HashMap的构造函数
HashMap一共提供了四个构造函数,其中默认无参的构造函数和参数为HashMap的构造函数为JavaCollectionFramework规范的推荐实现,其余两个构造函数则是HashMap专门提供的。
1、HashMap()
该构造函数意在构造一个具有默认初始容量(16)和默认负载因子(0.75)的空HashMap,是JavaCollectionFramework规范推荐提供的,其源码如下:
/**
*ConstructsanemptyHashMapwiththedefaultinitialcapacity
*(16)andthedefaultloadfactor(0.75).
*/
publicHashMap(){
//负载因子:
用于衡量的是一个散列表的空间的使用程度
this.loadFactor=DEFAULT_LOAD_FACTOR;
//HashMap进行扩容的阈值,它的值等于HashMap的容量乘以负载因子
threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
//HashMap的底层实现仍是数组,只是数组的每一项都是一条链
table=newEntry[DEFAULT_INITIAL_CAPACITY];
init();
}
2、HashMap(intinitialCapacity,floatloadFactor)
该构造函数意在构造一个指定初始容量和指定负载因子的空HashMap,其源码如下:
*ConstructsanemptyHashMapwiththespecifiedinitialcapacityandloadfactor.
publicHashMap(intinitialCapacity,floatloadFactor){
//初始容量不能小于0
if(initialCapacity<
0)
thrownewIllegalArgumentException("
Illegalinitialcapacity:
"
+initialCapacity);
//初始容量不能超过2^30
if(initialCapacity>
MAXIMUM_CAPACITY)
initialCapacity=MAXIMUM_CAPACITY;
//负载因子不能小于0
if(loadFactor<
=0||Float.isNaN(loadFactor))
Illegalloadfactor:
+
loadFactor);
//HashMap的容量必须是2的幂次方,超过initialCapacity的最小2^n
intcapacity=1;
while(capacity<
initialCapacity)
capacity<
<
=1;
//负载因子
this.loadFactor=loadFactor;
//设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行自动扩容操作
threshold=(int)(capacity*loadFactor);
table=newEntry[capacity];
3、HashMap(intinitialCapacity)
该构造函数意在构造一个指定初始容量和默认负载因子(0.75)的空HashMap,其源码如下:
//ConstructsanemptyHashMapwiththespecifiedinitialcapacityandthedefaultloadfactor(0.75)
publicHashMap(intinitialCapacity){
this(initialCapacity,DEFAULT_LOAD_FACTOR);
//直接调用上述构造函数
4、HashMap(Map<
?
extendsK,?
extendsV>
m)
该构造函数意在构造一个与指定Map具有相同映射的HashMap,其初始容量不小于16(具体依赖于指定Map的大小),负载因子是0.75,是JavaCollectionFramework规范推荐提供的,其源码如下:
//ConstructsanewHashMapwiththesamemappingsasthespecifiedMap.
//TheHashMapiscreatedwithdefaultloadfactor(0.75)andaninitialcapacity
//sufficienttoholdthemappingsinthespecifiedMap.
publicHashMap(Map<
m){
//初始容量不小于16
this(Math.max((int)(m.size()/DEFAULT_LOAD_FACTOR)+1,
DEFAULT_INITIAL_CAPACITY),DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
在这里,我们提到了两个非常重要的参数:
初始容量和负载因子,这两个参数是影响HashMap性能的重要参数。
其中,容量表示哈希表中桶的数量(table数组的大小),初始容量是创建哈希表时桶的数量;
负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于使用拉链法(下文会提到)的哈希表来说,查找一个元素的平均时间是O(1+a),a指的是链的长度,是一个常数。
特别地,若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;
若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。
系统默认负载因子为0.75,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。
四.HashMap的数据结构
1、哈希的相关概念
Hash就是把任意长度的输入(又叫做预映射,pre-image),通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。
这种转换是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间。
不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。
简单的说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要函数。
2、哈希的应用:
数据结构
我们知道,数组的特点是:
寻址容易,插入和删除困难;
而链表的特点是:
寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构呢?
答案是肯定的,这就是我们要提起的哈希表。
事实上,哈希表有多种不同的实现方法,我们接下来解释的是最经典的一种方法——拉链法,我们可以将其理解为链表的数组,如下图所示:
我们可以从上图看到,左边很明显是个数组,数组的每个成员是一个链表。
该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。
我们根据元素的自身特征把元素分配到不同的链表中去,反过来我们也正是通过这些特征找到正确的链表,再从链表中找出正确的元素。
其中,根据元素特征计算元素数组下标的方法就是哈希算法。
总的来说,哈希表适合用作快速查找、删除的基本数据结构,通常需要总数据量可以放入内存。
在使用哈希表时,有以下几个关键点:
hash函数(哈希算法)的选择:
针对不同的对象(字符串、整数等)具体的哈希方法;
碰撞处理:
常用的有两种方式,一种是openhashing,即拉链法;
另一种就是closedhashing,即开地址法(openedaddressing)。
3、HashMap的数据结构
我们知道,在Java中最常用的两种结构是数组和链表,几乎所有的数据结构都可以利用这两种来组合实现,HashMap就是这种应用的一个典型。
实际上,HashMap就是一个链表数组,如下是它数据结构:
从上图中,我们可以形象地看出HashMap底层实现还是数组,只是数组的每一项都是一条链。
其中参数initialCapacity就代表了该数组的长度,也就是桶的个数。
在第三节我们已经了解了HashMap的默认构造函数的源码:
从上述源码中我们可以看出,每次新建一个HashMap时,都会初始化一个Entry类型的table数组,其中Entry类型的定义如下:
staticclassEntry<
implementsMap.Entry<
{
finalKkey;
//键值对的键
Vvalue;
//键值对的值
Entry<
next;
//下一个节点
finalinthash;
//hash(key.hashCode())方法的返回值
*Createsnewentry.
Entry(inth,Kk,Vv,Entry<
n){//Entry的构造函数
value=v;
next=n;
key=k;
hash=h;
......
其中,Entry为HashMap的内部类,实现了Map.Entry接口,其包含了键key、值value、下一个节点next,以及hash值四个属性。
事实上,Entry是构成哈希表的基石,是哈希表所存储的元素的具体形式。
五.HashMap的快速存取
在HashMap中,我们最常用的两个操作就是:
put(Key,Value)和get(Key)。
我们都知道,HashMap中的Key是唯一的,那它是如何保证唯一性的呢?
我们首先想到的是用equals比较,没错,这样可以实现,但随着元素的增多,put和get的效率将越来越低,这里的时间复杂度是O(n)。
也就是说,假如HashMap有1000个元素,那么put时就需要比较1000次,这是相当耗时的,远达不到HashMap快速存取的目的。
实际上,HashMap很少会用到equals方法,因为其内通过一个哈希表管理所有元素,利用哈希算法可以快速的存取元素。
当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为bucketIndex。
通过《Java中的==,equals与hashCode的区别与联系》所述hashCode的协定可以知道,如果两个对象的hashCode不同,那么equals一定为false;
否则,如果其hashCode相同,equals也不一定为true。
所以,理论上,hashCode可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode()和equals()来逐个比较以判断Key是否已存在。
如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;
如果不存在,则存放新的键值对<
Key,Value>
到桶中。
因此,在HashMap中,equals()方法只有在哈希码碰撞时才会被用到。
下面我们结合JDK源码看HashMap的存取实现。
1、HashMap的存储实现
在HashMap中,键值对的存储是通过put(key,vlaue)方法来实现的,其源码如下:
*Associatesthespecifiedvaluewiththespecifiedkeyinthismap.
*Ifthemappreviouslycontainedamappingforthekey,theold
*valueisreplaced.
*
*@paramkeykeywithwhichthespecifiedvalueistobeassociated
*@paramvaluevaluetobeassociatedwiththespecifiedkey
*@returnthepreviousvalueassociatedwithkey,ornulliftherewasnomappingforkey.
*Notethatanullreturncanalsoindicatethatthemappreviouslyassociatednullwithkey.
publicVput(Kkey,Vvalue){
//当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
if(key==null)
returnputForNullKey(value);
//根据key的hashCode计算hash值
inthash=hash(key.hashCode());
//-------
(1)
//计算该键值对在数组中的存储位置(哪个桶)
inti=indexFor(hash,table.length);
//-------
(2)
//在table的第i个桶上进行迭代,寻找key保存的位置
for(Entry<
e=table[i];
e!
=null;
e=e.next){//-------(3)
Objectk;
//判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖value,并返回旧lue
if(e.hash==hash&
&
((k=e.key)==key||key.equals(k))){
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
//返回旧值
modCount++;
//修改次数增加1,快速失败机制
//原HashMap中无该映射,将该添加至该链的链头
addEntry(hash,key,value,i);
returnnull;
通过上述源码我们可以清楚了解到HashMap保存数据的过程:
首先,判断key是否为null,若为null,则直接调用putForNullKey方法;
若不为空,则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则查找是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。
此外,若table在该处没有元素,则直接保存。
这个过程看似比较简单,但其实有很多需要回味的地方,下面我们一一来看。
先看源码中的(3)处,此处迭代原因就是为了防止存在相同的key值。
如果发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这正好解释了HashMap中没有两个相同的key。
1).对NULL键的特别处理:
putForNullKey()
我们直接看其源码:
*Offloadedversionofputfornullkeys
privateVputForNullKey(Vvalue){
//若key==null,则将其放入table的第一个桶,即table[0]
e=table[0];
e=e.next){
if(e.key==null){//若已经存在key为null的键,则替换其值,并返回旧值
//快速失败
addEntry(0,null,value,0);
//否则,将其添加到table[0]的桶中
通过上述源码我们可以清楚知到,HashMap中可以保存键为NULL的键值对,且该键值对是唯一的。
若再次向其中添加键为NULL的键值对,将覆盖其原值。
此外,如果HashMap中存在键为NULL的键值对,那么一定在第一个桶中。
2).HashMap中的哈希策略(算法)
在上述的put(key,vlaue)方法的源码中,我们标出了HashMap中的哈希策略(即
(1)、
(2)两处),hash()方法用于对Key的hashCode进行重新计算,而indexFor()方法用于生成这个Entry对象的插入位置。
当计算出来的hash值与hashMap的(length-1)做了&
运算后,会得到位于区间[0,length-1]的一个值。
特别地,这个值分布的越均匀,HashMap的空间利用率也就越高,存取效率也就越好。
我们首先看
(1)处的hash()方法,该方法为一个纯粹的数学计算,用于进一步计算key的hash值,源码如下:
*AppliesasupplementalhashfunctiontoagivenhashCode,which
*defendsagainstpoorqualityhashfunctions.Thisiscritical
*becauseHashMapusespower-of-twolengthhashtables,that
*otherwiseencountercollisionsforhashCodesthatdonotdiffer
*inerbits.
*
*Note:
Nullkeysalwaysmaptohash0,thusindex0.
staticinthash(inth){
//ThisfunctionensuresthathashCodesthatdifferonlyby
//constantmultiplesateachbitpositionhaveabounded
//numberofcollisions(approximately8atdefaultloa