java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx

上传人:b****8 文档编号:12396461 上传时间:2023-06-05 格式:DOCX 页数:22 大小:94.04KB
下载 相关 举报
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第1页
第1页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第2页
第2页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第3页
第3页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第4页
第4页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第5页
第5页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第6页
第6页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第7页
第7页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第8页
第8页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第9页
第9页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第10页
第10页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第11页
第11页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第12页
第12页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第13页
第13页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第14页
第14页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第15页
第15页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第16页
第16页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第17页
第17页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第18页
第18页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第19页
第19页 / 共22页
java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx_第20页
第20页 / 共22页
亲,该文档总共22页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx

《java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx》由会员分享,可在线阅读,更多相关《java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx(22页珍藏版)》请在冰点文库上搜索。

java面试 集合中知识点 HashMapJDK18源码+底层数据结构分析 整理.docx

java面试集合中知识点HashMapJDK18源码+底层数据结构分析整理

•HashMap简介

•底层数据结构分析

–JDK1.8之前

–JDK1.8之后

•HashMap源码分析

–构造方法

–put方法

–get方法

–resize方法

•HashMap常用方法测试

感谢changfubai对本文的改进做出的贡献!

HashMap简介

HashMap主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。

JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8之后HashMap的组成多了红黑树,在满足下面两个条件之后,会执行链表转红黑树操作,以此来加快搜索速度。

•链表长度大于阈值(默认为8)

•HashMap数组长度超过64

底层数据结构分析

JDK1.8之前

JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列。

HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是HashMap的hash方法。

使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法换句话说使用扰动函数之后可以减少碰撞。

JDK1.8HashMap的hash方法源码:

JDK1.8的hash方法相比于JDK1.7hash方法更加简化,但是原理不变。

staticfinalinthash(Objectkey){

inth;

//key.hashCode():

返回散列值也就是hashcode

//^:

按位异或

//>>>:

无符号右移,忽略符号位,空位都以0补齐

return(key==null)?

0:

(h=key.hashCode())^(h>>>16);

}

对比一下JDK1.7的HashMap的hash方法源码.

staticinthash(inth){

//ThisfunctionensuresthathashCodesthatdifferonlyby

//constantmultiplesateachbitpositionhaveabounded

//numberofcollisions(approximately8atdefaultloadfactor).

h^=(h>>>20)^(h>>>12);

returnh^(h>>>7)^(h>>>4);

}

相比于JDK1.8的hash方法,JDK1.7的hash方法的性能会稍差一点点,因为毕竟扰动了4次。

所谓“拉链法”就是:

将链表和数组相结合。

也就是说创建一个链表数组,数组中每一格就是一个链表。

若遇到哈希冲突,则将冲突的值加到链表中即可。

jdk1.8之前的内部结构

JDK1.8之后

相比于之前的版本,JDK1.8以后在解决哈希冲突时有了较大的变化。

类的属性:

publicclassHashMapextendsAbstractMapimplementsMap,Cloneable,Serializable{

//序列号

privatestaticfinallongserialVersionUID=362498820763181265L;

//默认的初始容量是16

staticfinalintDEFAULT_INITIAL_CAPACITY=1<<4;

//最大容量

staticfinalintMAXIMUM_CAPACITY=1<<30;

//默认的填充因子

staticfinalfloatDEFAULT_LOAD_FACTOR=0.75f;

//当桶(bucket)上的结点数大于这个值时会转成红黑树

staticfinalintTREEIFY_THRESHOLD=8;

//当桶(bucket)上的结点数小于这个值时树转链表

staticfinalintUNTREEIFY_THRESHOLD=6;

//桶中结构转化为红黑树对应的table的最小大小

staticfinalintMIN_TREEIFY_CAPACITY=64;

//存储元素的数组,总是2的幂次倍

transientNode[]table;

//存放具体元素的集

transientSet>entrySet;

//存放元素的个数,注意这个不等于数组的长度。

transientintsize;

//每次扩容和更改map结构的计数器

transientintmodCount;

//临界值当实际大小(容量*填充因子)超过临界值时,会进行扩容

intthreshold;

//加载因子

finalfloatloadFactor;

}

•loadFactor加载因子

loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。

loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

给定的默认容量为16,负载因子为0.75。

Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12就需要将当前16的容量进行扩容,而扩容这个过程涉及到rehash、复制数据等操作,所以非常消耗性能。

•threshold

threshold=capacity*loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。

Node节点类源码:

//继承自Map.Entry

staticclassNodeimplementsMap.Entry{

finalinthash;//哈希值,存放元素到hashmap中时用来与其他元素hash值比较

finalKkey;//键

Vvalue;//值

//指向下一个节点

Nodenext;

Node(inthash,Kkey,Vvalue,Nodenext){

this.hash=hash;

this.key=key;

this.value=value;

this.next=next;

}

publicfinalKgetKey(){returnkey;}

publicfinalVgetValue(){returnvalue;}

publicfinalStringtoString(){returnkey+"="+value;}

//重写hashCode()方法

publicfinalinthashCode(){

returnObjects.hashCode(key)^Objects.hashCode(value);

}

publicfinalVsetValue(VnewValue){

VoldValue=value;

value=newValue;

returnoldValue;

}

//重写equals()方法

publicfinalbooleanequals(Objecto){

if(o==this)

returntrue;

if(oinstanceofMap.Entry){

Map.Entry

?

>e=(Map.Entry

?

>)o;

if(Objects.equals(key,e.getKey())&&

Objects.equals(value,e.getValue()))

returntrue;

}

returnfalse;

}

}

树节点类源码:

staticfinalclassTreeNodeextendsLinkedHashMap.Entry{

TreeNodeparent;//父

TreeNodeleft;//左

TreeNoderight;//右

TreeNodeprev;//neededtounlinknextupondeletion

booleanred;//判断颜色

TreeNode(inthash,Kkey,Vval,Nodenext){

super(hash,key,val,next);

}

//返回根节点

finalTreeNoderoot(){

for(TreeNoder=this,p;;){

if((p=r.parent)==null)

returnr;

r=p;

}

HashMap源码分析

构造方法

HashMap中有四个构造方法,它们分别如下:

//默认构造函数。

publicHashMap(){

this.loadFactor=DEFAULT_LOAD_FACTOR;//allotherfieldsdefaulted

}

//包含另一个“Map”的构造函数

publicHashMap(Map

extendsK,?

extendsV>m){

this.loadFactor=DEFAULT_LOAD_FACTOR;

putMapEntries(m,false);//下面会分析到这个方法

}

//指定“容量大小”的构造函数

publicHashMap(intinitialCapacity){

this(initialCapacity,DEFAULT_LOAD_FACTOR);

}

//指定“容量大小”和“加载因子”的构造函数

publicHashMap(intinitialCapacity,floatloadFactor){

if(initialCapacity<0)

thrownewIllegalArgumentException("Illegalinitialcapacity:

"+initialCapacity);

if(initialCapacity>MAXIMUM_CAPACITY)

initialCapacity=MAXIMUM_CAPACITY;

if(loadFactor<=0||Float.isNaN(loadFactor))

thrownewIllegalArgumentException("Illegalloadfactor:

"+loadFactor);

this.loadFactor=loadFactor;

this.threshold=tableSizeFor(initialCapacity);

}

putMapEntries方法:

finalvoidputMapEntries(Map

extendsK,?

extendsV>m,booleanevict){

ints=m.size();

if(s>0){

//判断table是否已经初始化

if(table==null){//pre-size

//未初始化,s为m的实际元素个数

floatft=((float)s/loadFactor)+1.0F;

intt=((ft<(float)MAXIMUM_CAPACITY)?

(int)ft:

MAXIMUM_CAPACITY);

//计算得到的t大于阈值,则初始化阈值

if(t>threshold)

threshold=tableSizeFor(t);

}

//已初始化,并且m元素个数大于阈值,进行扩容处理

elseif(s>threshold)

resize();

//将m中的所有元素添加至HashMap中

for(Map.Entry

extendsK,?

extendsV>e:

m.entrySet()){

Kkey=e.getKey();

Vvalue=e.getValue();

putVal(hash(key),key,value,false,evict);

}

}

}

put方法

HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

对putVal方法添加元素的分析如下:

1.如果定位到的数组位置没有元素就直接插入。

2.如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e=((TreeNode)p).putTreeVal(this,tab,hash,key,value)将元素添加进入。

如果不是就遍历链表插入(插入的是链表尾部)。

说明:

上图有两个小问题:

•直接覆盖之后应该就会return,不会有后续操作。

参考JDK8HashMap.java658行(issue#608)。

•当链表长度大于阈值(默认为8)并且HashMap数组长度超过64的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。

参考HashMap的treeifyBin()方法(issue#1087)。

publicVput(Kkey,Vvalue){

returnputVal(hash(key),key,value,false,true);

}

finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,

booleanevict){

Node[]tab;Nodep;intn,i;

//table未初始化或者长度为0,进行扩容

if((tab=table)==null||(n=tab.length)==0)

n=(tab=resize()).length;

//(n-1)&hash确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)

if((p=tab[i=(n-1)&hash])==null)

tab[i]=newNode(hash,key,value,null);

//桶中已经存在元素

else{

Nodee;Kk;

//比较桶中第一个元素(数组中的结点)的hash值相等,key相等

if(p.hash==hash&&

((k=p.key)==key||(key!

=null&&key.equals(k))))

//将第一个元素赋值给e,用e来记录

e=p;

//hash值不相等,即key不相等;为红黑树结点

elseif(pinstanceofTreeNode)

//放入树中

e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);

//为链表结点

else{

//在链表最末插入结点

for(intbinCount=0;;++binCount){

//到达链表的尾部

if((e=p.next)==null){

//在尾部插入新结点

p.next=newNode(hash,key,value,null);

//结点数量达到阈值(默认为8),执行treeifyBin方法

//这个方法会根据HashMap数组来决定是否转换为红黑树。

//只有当数组长度大于或者等于64的情况下,才会执行转换红黑树操作,以减少搜索时间。

否则,就是只是对数组扩容。

if(binCount>=TREEIFY_THRESHOLD-1)//-1for1st

treeifyBin(tab,hash);

//跳出循环

break;

}

//判断链表中结点的key值与插入的元素的key值是否相等

if(e.hash==hash&&

((k=e.key)==key||(key!

=null&&key.equals(k))))

//相等,跳出循环

break;

//用于遍历桶中的链表,与前面的e=p.next组合,可以遍历链表

p=e;

}

}

//表示在桶中找到key值、hash值与插入元素相等的结点

if(e!

=null){

//记录e的value

VoldValue=e.value;

//onlyIfAbsent为false或者旧值为null

if(!

onlyIfAbsent||oldValue==null)

//用新值替换旧值

e.value=value;

//访问后回调

afterNodeAccess(e);

//返回旧值

returnoldValue;

}

}

//结构性修改

++modCount;

//实际大小大于阈值则扩容

if(++size>threshold)

resize();

//插入后回调

afterNodeInsertion(evict);

returnnull;

}

我们再来对比一下JDK1.7put方法的代码

对于put方法的分析如下:

•①如果定位到的数组位置没有元素就直接插入。

•②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。

publicVput(Kkey,Vvalue)

if(table==EMPTY_TABLE){

inflateTable(threshold);

}

if(key==null)

returnputForNullKey(value);

inthash=hash(key);

inti=indexFor(hash,table.length);

for(Entrye=table[i];e!

=null;e=e.next){//先遍历

Objectk;

if(e.hash==hash&&((k=e.key)==key||key.equals(k))){

VoldValue=e.value;

e.value=value;

e.recordAccess(this);

returnoldValue;

}

}

modCount++;

addEntry(hash,key,value,i);//再插入

returnnull;

}

get方法

publicVget(Objectkey){

Nodee;

return(e=getNode(hash(key),key))==null?

null:

e.value;

}

finalNodegetNode(inthash,Objectkey){

Node[]tab;Nodefirst,e;intn;Kk;

if((tab=table)!

=null&&(n=tab.length)>0&&

(first=tab[(n-1)&hash])!

=null){

//数组元素相等

if(first.hash==hash&&//alwayscheckfirstnode

((k=first.key)==key||(key!

=null&&key.equals(k))))

returnfirst;

//桶中不止一个节点

if((e=first.next)!

=null){

//在树中get

if(firstinstanceofTreeNode)

return((TreeNode)first).getTreeNode(hash,key);

//在链表中get

do{

if(e.hash==hash&&

((k=e.key)==key||(key!

=null&&key.equals(k))))

returne;

}while((e=e.next)!

=null);

}

}

returnnull;

}

resize方法

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。

在编写程序中,要尽量避免resize。

finalNode[]resize(){

Node[]oldTab=table;

intoldCap=(oldTab==null)?

0:

oldTab.length;

intoldThr=threshold;

intnewCap,newThr=0;

if(oldCap>0){

//超过最大值就不再扩充

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

当前位置:首页 > 高中教育 > 其它课程

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2