HashMap

5.1、对于HashMap需要掌握以下几点

Map的创建:HashMap()

往Map中添加键值对:即put(Object key, Object value)方法

获取Map中的单个对象:即get(Object key)方法

删除Map中的对象:即remove(Object key)方法

判断对象是否存在于Map中:containsKey(Object key)

遍历Map中的对象:即keySet(),在实际中更常用的是增强型的for循环去做遍历

Map中对象的排序:主要取决于所采取的排序算法

5.2、构建HashMap

源代码:

一些属性:

staticfinalintDEFAULT_INITIAL_CAPACITY = 16;//默认的初始化容量(必须是2的多少次方)staticfinalintMAXIMUM_CAPACITY = 1 << 30;//最大指定容量为2的30次方staticfinalfloatDEFAULT_LOAD_FACTOR = 0.75f;//默认的加载因子(用于resize)transientEntry[] table;//Entry数组(数组容量必须是2的多少次方,若有必要会扩容resize)--这就是HashMap的底层数据结构transientintsize;//该map中存放的key-value对个数,该个数决定了数组的扩容(而非table中的所占用的桶的个数来决定是否扩容)//扩容resize的条件:eg.capacity=16,load_factor=0.75,threshold=capacity*load_factor=12,即当该map中存放的key-value对个数size>=12时,就resize)intthreshold;finalfloatloadFactor;//负载因子(用于resize)transientvolatileintmodCount;//标志位,用于标识并发问题

注意:

map中存放的key-value对个数size,该个数决定了数组的扩容(size>=threshold时,扩容),而非table中的所占用的桶的个数来决定是否扩容

标志位modCount采用volatile实现该变量的线程可见性(之后会在"Java并发"章节中去讲)

数组中的桶,指的就是table[i]

无参构造器(也是当下最常用的构造器)

/*** 初始化一个负载因子、resize条件和Entry数组*/publicHashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;//负载因子:0.75threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//当该map中存放的key-value对个数size>=12时,就resizetable =newEntry[DEFAULT_INITIAL_CAPACITY];//设置Entry数组容量为16init();

}

注意:

init()为空方法

对于hashmap而言,还有两个比较常用的构造器,一个双参,一个单参。

/*** 指定初始容量和负载因子*/publicHashMap(intinitialCapacity,floatloadFactor) {if(initialCapacity < 0)thrownewIllegalArgumentException("Illegal initial capacity:"+initialCapacity);if(initialCapacity >MAXIMUM_CAPACITY)

initialCapacity=MAXIMUM_CAPACITY;if(loadFactor <= 0 || Float.isNaN(loadFactor))//loadFactor<0或者不是一个值thrownewIllegalArgumentException("Illegal load factor:"+loadFactor);/** 下边的逻辑是找一个2的几次方的数,该数刚刚大于initialCapacity

* eg.当指定initialCapacity为17,capacity就是32(2的五次方),而2的四次方(16)正好小于17*/intcapacity = 1;while(capacity

capacity<<= 1;//capacity = capacity<<1this.loadFactor =loadFactor;

threshold= (int)(capacity *loadFactor);

table=newEntry[capacity];

init();

}/*** 指定初始容量*/publicHashMap(intinitialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上边的双参构造器}

注意:

利用上述两个构造器构造出的数组容量不一定是指定的初始化容量,而是一个刚刚大于指定初始化容量的2的几次方的一个值。

在实际使用中,若我们能预判所要存储的元素的多少,最好使用上述的单参构造器来指定初始容量,这样的话,就可以避免就来扩容时带来的消耗(这一点与ArrayList一样

HashMap的底层数据结构是一个Entry[],Entry是HashMap的一个内部类,源代码如下:

staticclassEntryimplementsMap.Entry{finalK key;//该Entry的keyV value;//该Entry的valueEntry next;//该Entry的下一个Entry(hash冲突时,形成链表)finalinthash;//该Entry的hash值/*** Creates new entry.*/Entry(inth, K k, V v, Entryn) {

value=v;

next=n;

key=k;

hash=h;

}publicfinalK getKey() {returnkey;

}publicfinalV getValue() {returnvalue;

}//为Entry设置新的valuepublicfinalV setValue(V newValue) {

V oldValue=value;

value=newValue;returnoldValue;

}publicfinalbooleanequals(Object o) {if(!(oinstanceofMap.Entry))returnfalse;

Map.Entry e=(Map.Entry) o;

Object k1=getKey();

Object k2=e.getKey();//在hashmap中可以存放null键和null值if(k1 == k2 || (k1 !=null&&k1.equals(k2))) {

Object v1=getValue();

Object v2=e.getValue();if(v1 == v2 || (v1 !=null&&v1.equals(v2)))returntrue;

}returnfalse;

}publicfinalinthashCode() {return(key ==null? 0 : key.hashCode())^(value ==null? 0: value.hashCode());

}publicfinalString toString() {returngetKey() + "=" +getValue();

}

}

注:这里我去掉了两个空方法。

Entry是一个节点,在其中还保存了下一个Entry的引用(用来解决put时的hash冲突问题),这样的话,我们可以把hashmap看作是"一个链表数组"

Entry类中的equals()方法会在get(Object key)中使用

5.3、put(Object key, Object value)

源代码:

put(Object key, Object value)

/*** 向map中添加新Entry

* 步骤:

* 1)HashMap可以添加null的key,key==null的Entry只会放在table[0]中,但是table[0]不仅仅可以存放key==null的Entry

* 1.1、遍历table[0]中的Entry链,若有key==null的值就用新值覆盖旧值,并返回旧值value,

* 1.2、若无,执行addEntry方法,用新的Entry替换掉原来旧的Entry赋值给table[0],而旧的Entry作为新的Entry的next,执行结束后,返回null

* 2)添加key!=null的Entry时,

* 2.1、先计算key.hashCode()的hash值,

* 2.2、然后计算出将要放入的table的下标i,

* 2.3、之后遍历table[i]中的Entry链,若有相同key的值就用新值覆盖旧值,并返回旧值value,

* 2.4、若无,执行addEntry方法,用新的Entry替换掉原来旧的Entry赋值给table[i],而旧的Entry作为新的Entry的next,执行结束后,返回null*/publicV put(K key, V value) {/******************key==null******************/if(key ==null)returnputForNullKey(value);//将空key的Entry加入到table[0]中/******************key!=null******************/inthash = hash(key.hashCode());//计算key.hashcode()的hash值,hash函数由hashmap自己实现inti = indexFor(hash, table.length);//获取将要存放的数组下标/** for中的代码用于:当hash值相同且key相同的情况下,使用新值覆盖旧值(其实就是修改功能)*/for(Entry e = table[i]; e !=null; e = e.next) {//注意:for循环在第一次执行时就会先判断条件Object k;//hash值相同且key相同的情况下,使用新值覆盖旧值if(e.hash == hash && ((k = e.key) == key ||key.equals(k))) {

V oldValue=e.value;

e.value=value;//e.recordAccess(this);returnoldValue;//返回旧值}

}

modCount++;

addEntry(hash, key, value, i);//增加一个新的Entry到table[i]returnnull;//如果没有与传入的key相等的Entry,就返回null}

注意:该方法头部的注释写明了整个put(Object key, Object value)的流程,非常重要

putForNullKey(V value)

/*** 增加null的key到table[0]*/privateV putForNullKey(V value) {//遍历第一个数组元素table[0]中的所有Entry节点for(Entry e = table[0]; e !=null; e =e.next) {if(e.key ==null) {//用新值覆盖旧值V oldValue =e.value;

e.value=value;//e.recordAccess(this);returnoldValue;

}

}

modCount++;

addEntry(0,null, value, 0);//将新节点Entry加入到Entry[]中returnnull;

}

addEntry(int hash, K key, V value, int bucketIndex)

/*** 添加新的Entry到table[bucketIndex]*/voidaddEntry(inthash, K key, V value,intbucketIndex) {/** 这里可以看出,

* 1)新加入的Entry会放入链头,也就是说将来遍历的时候,最先加入map的反而是最后被遍历到的

* 2)采用的是Entry替换的方式

* 2.1、当添加第一个Entry1时,table[bucketIndex]==null,也就是说Entry1的下一个Entry为null(链尾),之后把table[bucketIndex] = Entry1

* 2.2、当添加第二个Entry2时,table[bucketIndex]==Entry1,也就是说Entry2的下一个Entry为Entry1,之后把table[bucketIndex] = Entry2

* 2.3、当添加第三个Entry3时,table[bucketIndex]==Entry2,也就是说Entry3的下一个Entry为Entry2,之后把table[bucketIndex] = Entry3*/Entry e = table[bucketIndex];//新节点的下一个节点(当第一次在相应的数组位置放置元素时,table[bucketIndex]==null)table[bucketIndex] =newEntry(hash, key, value, e);if(size++ >= threshold)//key-value对个数大于等于thresholdresize(2 * table.length);//扩容}

注意:该方法头部的注释写明了该方法的流程示例,可以自己画个图对比着理解

hash(int h)

/*** hash函数,用于计算key.hashCode()的hash值

* Note: null的key的hash为0,放在table[0].*/staticinthash(inth) {//这样的hash函数应该可以尽量将hash值打散h ^= (h >>> 20) ^ (h >>> 12);returnh ^ (h >>> 7) ^ (h >>> 4);

}

注意:在我们实际使用hashmap时,最好的情况是将key的hash值打散,使插入的这些Entry尽量落在不同的桶上(这样做的主要目的是提高查询效率),以上这个hash函数应该就是实现了这样的功能,但是为什么这样的hash函数可以将hash值打散,求大神指点!!!

indexFor(int h, int length)

/*** "按位与"来获取数组下标*/staticintindexFor(inth,intlength) {returnh & (length - 1);

}

说明:在上述的addEntry(int hash, K key, V value, int bucketIndex)方法中,我们可以看到,当为把新的Entry赋值给table[i]后,会判断map中的key-value对是不是已经大于等于扩容条件值threshold了,若是,则需要调用resize函数,对Entry数组进行扩容,扩为原来二倍。

resize(int newCapacity)

/*** 扩容步骤:

* 1)数组扩容为原来容量(eg.16)的二倍

* 2)将旧数组中的所有Entry重新计算索引,加入新数组

* 3)将新数组的引用赋给旧数组

* 4)重新计算扩容临界值threshold*/voidresize(intnewCapacity) {

Entry[] oldTable=table;intoldCapacity =oldTable.length;//如果旧的数组的容量为2的30次方(这种情况,不考虑了,如果真达到这样的情况,性能下降的就不像话了)if(oldCapacity ==MAXIMUM_CAPACITY) {

threshold=Integer.MAX_VALUE;return;

}

Entry[] newTable=newEntry[newCapacity];//newCapacity==2*oldCapacitytransfer(newTable);//将旧数组中的所有Entry重新计算索引,加入新数组table = newTable;//将新数组赋给就数组threshold = (int) (newCapacity * loadFactor);//重新计算threshold}

transfer(Entry[] newTable)

jdk中的实现:

/*** 将所有旧的数组中的所有Entry移动到新数组中去*/voidtransfer(Entry[] newTable) {

Entry[] src=table;intnewCapacity =newTable.length;for(intj = 0; j < src.length; j++) {//遍历旧数组Entry e = src[j];//获得头节点if(e !=null) {/** 这样写,若同时有其他线程还在访问这个元素,则访问不到了,这里这样写,是考虑到多线程情况下,我们一般不会会用HashMap

* (查看ConcurrentHashMap并未将旧数组的值置为null)

* 这里将其置为null就方便gc回收

* 当然为了减小以上所说的影响,建议将src[j] = null;放在while循环结束后*/src[j]=null;do{

Entry next =e.next;inti =indexFor(e.hash, newCapacity);

e.next= newTable[i];//把之前已经存在的newTable[i]的元素赋给当前节点的下一个节点newTable[i] = e;//把当前节点赋给newTable[i]e =next;

}while(e !=null);//遍历链表}

}

}

我的修改:(注意:这是一个错误的修改,错误的根源在下边我会给出

/*** 将所有旧的数组中的所有Entry移动到新数组中去*/voidtransfer(Entry[] newTable) {

Entry[] src= table;//旧数组intnewCapacity = newTable.length;//新数组容量for(intj = 0; j < src.length; j++) {

Entry e = src[j];//获取旧数组中的头节点Entryif(e !=null) {

src[j]=null;//将旧数组置空,让gc回收注意:这个时候table的桶并没有置空/** 根据旧的hash值与新的容量值进行重新定位(注意:并没有重新计算hash值)

* 1、那么假设之前table[1]中存放的是Entry3,Entry3.next是Entry2,Entry2.next是Entry1,Entry1.next是null

*  那么假设重新计算后的i=3,那么Entry3-->Entry2-->Entry1依旧会在一起,都放入newTable[3],这样的话,我们只需要将链头的Entry3赋值给newTable[3]即可

* 2、既然通过indexFor(e.hash, newCapacity)不能把同一个桶下的Entry打散,为什么还要用呢?

*    主要是扩容后,若不用newCapacity去计算下标的话,那么扩容后,map中的Entry就都集中在了新数组的前半部分,这样就不够散了*/inti =indexFor(e.hash, newCapacity);

newTable[i]= e;//将Entry3赋值给newTable[3]}

}

}

注意:

在这个方法中,并没有重新计算hash值,只是重新计算了下标索引。

错误根源在于认为同一个桶下的所有Entry的hash值相同,事实上不相同,只是hash&(table.length-1)的结果相同,

所以当table.length发生变化时,同一个桶下各个Entry算出来的index会不同(即Entry3、Entry2、Entry1可能会落在新数组的不同的桶上

5.4、get(Object key)

源代码:

get(Object key)

/*** 查找指定key的value值

* 1、若key==null

* 遍历table[0],找出key==null的value,若没找到,返回null

* 2、若key!=null

* 1)计算key.hashCode()的hash值

* 2)根据计算出的hash值和数组容量,调用indexFor方法,获得table的下标i,进而获得桶table[i]

* 3)遍历该桶中的每一个Entry,找出key相等(==或equals)的Entry,获取此Entry的value即可

* 4)最后,若没有找到,返回null即可*/publicV get(Object key) {/****************查找key==null的value****************/if(key ==null)returngetForNullKey();/****************查找key!=null的value****************/inthash = hash(key.hashCode());//获取key.hashCode()的hash值for(Entry e = table[indexFor(hash, table.length)]; e !=null; e =e.next) {

Object k;if(e.hash == hash && ((k = e.key) == key ||key.equals(k)))returne.value;

}returnnull;//若没有指定key的Entry,则直接返回null}

注意:查看代码头部的注释,表明了get的整个步骤

getForNullKey()

View Code

5.5、remove(Object key)

源代码:

/*** 删除指定key的Entry*/publicV remove(Object key) {

Entry e =removeEntryForKey(key);return(e ==null?null: e.value);//返回删除的节点(e为null的话,表示所给出的key不存在)}/*** 删除指定key的Entry

* 1)若删除的是头节点,例如Entry3,只需将Entry2赋值给table[i]即可

* 2)若删除的是中间节点,例如Entry2,只需将Entry3.next指向Entry2.next(即Entry1)即可

* 3)若删除的是尾节点,例如Entry1,只需将Entry2.next指向Entry1.next(即null)即可*/finalEntryremoveEntryForKey(Object key) {inthash = (key ==null) ? 0 : hash(key.hashCode());//计算hash值inti = indexFor(hash, table.length);//按位与计算下标Entry prev = table[i];//获取桶Entry e =prev;while(e !=null) {

Entry next =e.next;

Object k;if(e.hash == hash && ((k = e.key) == key || (key !=null&&key.equals(k)))) {

modCount++;

size--;//size-1if(prev == e)//删除头节点,即示例中的Entry3table[i] =next;else//删除除了头节点外的其他节点prev.next =next;//e.recordRemoval(this);returne;

}

prev=e;

e=next;

}returne;//返回删除的节点(e为null的话,表示所给出的key不存在)}

注:看注释即可,最好用示例去套一下代码。

若删除的key不存在于map中,返回null,不会抛异常。

5.6、containsKey(Object key)

源代码:

/*** 判断map是否包含指定可以的Entry*/publicbooleancontainsKey(Object key) {returngetEntry(key) !=null;

}/*** 判断map是否包含指定可以的Entry,与get(Object key)基本相同(只是这里将key==null与key!=null的情况写在了一起,get(Object key)也可以这样去做)

* 1)计算key.hashCode()的hash值

* 2)根据计算出的hash值和数组容量,调用indexFor方法,获得table的下标i,进而获得桶table[i]

* 3)遍历该桶中的每一个Entry,找出key相等(==或equals)的Entry,获取此Entry,并返回此Entry

* 4)最后,若没有找到,返回null即可*/finalEntrygetEntry(Object key) {inthash = (key ==null) ? 0 : hash(key.hashCode());//计算hash值for(Entry e = table[indexFor(hash, table.length)]; e !=null; e =e.next) {

Object k;if(e.hash == hash && ((k = e.key) == key || (key !=null&&key.equals(k))))returne;

}returnnull;

}

注意:此方法与get(Object key)基本相同,只是只是这里将key==null与key!=null的情况写在了一起,get(Object key)也可以这样去做来减少代码

5.7、keySet()

遍历所有Entry链表,获取每一个Entry的key,在整个过程中,如果发生了增删操作,抛出ConcurrentModificationException。

finalEntrynextEntry() {if(modCount !=expectedModCount)thrownewConcurrentModificationException();

Entry e =next;if(e ==null)thrownewNoSuchElementException();if((next = e.next) ==null) {

Entry[] t=table;while(index < t.length && (next = t[index++]) ==null)

;

}

current=e;returne;

}

总结:

HashMap底层就是一个Entry数组,Entry又包含next,事实上,可以看成是一个"链表数组"

扩容:map中存放的key-value对个数size,该个数决定了数组的扩容(size>=threshold时,扩容),而非table中的所占用的桶的个数来决定是否扩容

扩容过程,不会重新计算hash值,只会重新按位与

在实际使用中,若我们能预判所要存储的元素的多少,最好使用上述的单参构造器来指定初始容量

HashMap可以插入null的key和value

remove(Object key):若删除的key不存在于map中,返回null,不会抛异常。

HashMap线程不安全,若想要线程安全,最好使用ConcurrentHashMap

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容