Map 综述一彻头彻尾理解 HashMap.docx

上传人:b****6 文档编号:7200201 上传时间:2023-01-21 格式:DOCX 页数:18 大小:193.05KB
下载 相关 举报
Map 综述一彻头彻尾理解 HashMap.docx_第1页
第1页 / 共18页
Map 综述一彻头彻尾理解 HashMap.docx_第2页
第2页 / 共18页
Map 综述一彻头彻尾理解 HashMap.docx_第3页
第3页 / 共18页
Map 综述一彻头彻尾理解 HashMap.docx_第4页
第4页 / 共18页
Map 综述一彻头彻尾理解 HashMap.docx_第5页
第5页 / 共18页
点击查看更多>>
下载资源
资源描述

Map 综述一彻头彻尾理解 HashMap.docx

《Map 综述一彻头彻尾理解 HashMap.docx》由会员分享,可在线阅读,更多相关《Map 综述一彻头彻尾理解 HashMap.docx(18页珍藏版)》请在冰豆网上搜索。

Map 综述一彻头彻尾理解 HashMap.docx

Map综述一彻头彻尾理解HashMap

Map综述

(一):

彻头彻尾理解HashMap

一.HashMap概述

Map是Key-Value对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。

HashMap是JavaCollectionFramework的重要成员,也是Map族(如下图所示)中我们最为常用的一种。

简单地说,HashMap是基于哈希表的Map接口的实现,以Key-Value的形式存在,即存储的对象是Entry(同时包含了Key和Value)。

在HashMap中,其会根据hash算法来计算key-value的存储位置并进行快速存取。

特别地,HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。

此外,HashMap是Map的一个非同步的实现。

同样地,HashSet也是JavaCollectionFramework的重要成员,是Set接口的常用实现类,但其与HashMap有很多相似之处。

对于HashSet而言,其采用Hash算法决定元素在Set中的存储位置,这样可以保证元素的快速存取;对于HashMap而言,其将key-value当成一个整体(Entry对象)来处理,其也采用同样的Hash算法去决定key-value的存储位置从而保证键值对的快速存取。

虽然HashMap和HashSet实现的接口规范不同,但是它们底层的Hash存储机制完全相同。

实际上,HashSet本身就是在HashMap的基础上实现的。

因此,通过对HashMap的数据结构、实现原理、源码实现三个方面了解,我们不但可以进一步掌握其底层的Hash存储机制,也有助于对HashSet的了解。

  必须指出的是,虽然容器号称存储的是Java对象,但实际上并不会真正将Java对象放入容器中,只是在容器中保留这些对象的引用。

也就是说,Java容器实际上包含的是引用变量,而这些引用变量指向了我们要实际保存的Java对象。

二.HashMap在JDK中的定义

  HashMap实现了Map接口,并继承AbstractMap抽象类,其中Map接口定义了键值映射规则。

和AbstractCollection抽象类在Collection族的作用类似,AbstractMap抽象类提供了Map接口的骨干实现,以最大限度地减少实现Map接口所需的工作。

HashMap在JDK中的定义为:

publicclassHashMap

extendsAbstractMap

implementsMap,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))

thrownewIllegalArgumentException("Illegalloadfactor:

"+

loadFactor);

//HashMap的容量必须是2的幂次方,超过initialCapacity的最小2^n

intcapacity=1;

while(capacity

capacity<<=1;

//负载因子

this.loadFactor=loadFactor;

//设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行自动扩容操作

threshold=(int)(capacity*loadFactor);

//HashMap的底层实现仍是数组,只是数组的每一项都是一条链

table=newEntry[capacity];

init();

}

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

extendsK,?

extendsV>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的默认构造函数的源码:

/**

*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();

}

从上述源码中我们可以看出,每次新建一个HashMap时,都会初始化一个Entry类型的table数组,其中Entry类型的定义如下:

staticclassEntryimplementsMap.Entry{

finalKkey;//键值对的键

Vvalue;//键值对的值

Entrynext;//下一个节点

finalinthash;//hash(key.hashCode())方法的返回值

/**

*Createsnewentry.

*/

Entry(inth,Kk,Vv,Entryn){//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值;如果不存在,则存放新的键值对到桶中。

因此,在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(Entrye=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]

for(Entrye=table[0];e!

=null;e=e.next){

if(e.key==null){//若已经存在key为null的键,则替换其值,并返回旧值

VoldValue=e.value;

e.value=value;

e.recordAccess(this);

returnoldValue;

}

}

modCount++;//快速失败

addEntry(0,null,value,0);//否则,将其添加到table[0]的桶中

returnnull;

}

通过上述源码我们可以清楚知到,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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 环境科学食品科学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1