Map 综述一彻头彻尾理解 HashMap.docx
- 文档编号:7200201
- 上传时间:2023-01-21
- 格式:DOCX
- 页数:18
- 大小:193.05KB
Map 综述一彻头彻尾理解 HashMap.docx
《Map 综述一彻头彻尾理解 HashMap.docx》由会员分享,可在线阅读,更多相关《Map 综述一彻头彻尾理解 HashMap.docx(18页珍藏版)》请在冰豆网上搜索。
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
...
}
三.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类型的定义如下: staticclassEntry finalKkey;//键值对的键 Vvalue;//键值对的值 Entry finalinthash;//hash(key.hashCode())方法的返回值 /** *Createsnewentry. */ Entry(inth,Kk,Vv,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(Entry =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(Entry =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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Map 综述一彻头彻尾理解 HashMap 综述 彻头彻尾 理解