通過HashMap原理詳解entrySet中的疑問
HashMap底層變量
HashMap的底層的一些變量:
transient Node<K,V>[] table; //存儲數(shù)據(jù)的Node數(shù)組
transient Set<java.util.Map.Entry<K,V>> entrySet;
transient int size; //map中存放數(shù)據(jù)的個數(shù),不等于table.length
transient int modCount; //修改的次數(shù),防止
int threshold; //臨界值
final float loadFactor; //擴(kuò)展因子,一般情況下threshold=table.length*loadFactor;構(gòu)造一個空的HashMap時(shí),只有l(wèi)oadFactor被賦值為默認(rèn)的0.75。代碼如下:
public HashMapMmc(){
this.loadFactor=DEFAULT_LOAD_FACTOR;
}這里我將介紹三個方法,put get remove,最后介紹entrySet()遍歷。
put()方法:
在調(diào)用put(key,value)方法時(shí),底層調(diào)用的是這個方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n,i;
if((tab=table)==null||(n=tab.length)==0)
n=(tab=resize()).length;
if((p=tab[i=(n-1)&hash])==null)
tab[i]=newNode(hash,key,value,null);
else{
Node<K,V> e;K k;
if(p.hash==hash&&((k=p.key)==key||(k!=null&&k.equals(key))))
e=p;
else if(p instanceof TreeNode)
e=((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);
else{
for(int binCount=0;;++binCount){
if((e=p.next)==null){
p.next=newNode(hash,key,value,null);
if(binCount>=TREEIFY_THRESHOLD-1)
treeifyBin(tab,hash);
break;
}
if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))
break;
p=e;
}
}
if(e!=null){ // existing mapping for key
V oldValue=e.value;
if(!onlyIfAbsent||oldValue==null)
e.value=value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if(++size>threshold)
resize();
afterNodeInsertion(evict);
return null;
}這個方法有5個參數(shù),第一個為hash,可以理解為對key經(jīng)過運(yùn)算之后的一個值(具體算法:(key==null)?0:(h = key.hashCode())^(h>>>16)),第二個為key,第三個為value,這些都不用說了吧,第四個為onlyIfAbsent,這里代表的是是否覆蓋,如果為false,同樣的key放在map中,后面放入的值會覆蓋原來的值,put方法在調(diào)用這個putVal()方法時(shí),onlyIfAbsent寫死為false的,所以HashMap中,是沒有重復(fù)的key值的,后來的value會覆蓋原來的value??聪旅娣椒ǖ谒膫€參數(shù):
public V put(K key,V value ){
return putVal(hash(key),key,value,false,true);
}然后說放入過程:
先檢查table夠不夠存放數(shù)據(jù)。剛剛new出來的HashMap,table是為空的。在放入時(shí)會先進(jìn)行擴(kuò)容,按照默認(rèn)的大小16.
Node<K,V>[] newTab=(Node<K,V>[])new Node[newCap];
計(jì)算要放入的位置,HashMap是沒有順序的,默認(rèn)的16個索引位置中,會隨機(jī)的找一個放入。(注意:key是可以等于null的,key等于null時(shí),計(jì)算出來的索引是0)計(jì)算索引的方法是:
(n-1)&hash //n代表的是table的length,hash就是上面的第一個參數(shù)hash(key);
所謂的碰撞問題解析:正常情況下直接放入就行了,但是如果加入的元素和之前的元素計(jì)算出來的索引位置是一樣的。例如:新建一個HashMap,放入(1,"a")和(17,"b")時(shí),他們計(jì)算出來的索引相同,這時(shí)第一個Node放入好之后,第二個Node不會在重新在table中占一個索引了,會在同一個索引的Node上形成鏈表。即Node1.next=Node2. Node1和Node2都在table數(shù)組里同一個索引里面。如果在放入一個(33,"c"),這個其實(shí)也是和上面兩個計(jì)算出來是同一個索引位置,會放在Node2.next=Node3.
p.next=newNode(hash,key,value,null); //newNode方法會新聲明一個Node
2. get(Object key)方法:
知道了put方法,get(Object key)方法就比較簡單了,直接通過key算出他在table數(shù)組中的索引位置直接獲取就行了,因?yàn)橛锌赡芡粋€索引位置放了幾個元素,所以他會先找到第一個元素,然后對比hash和key是否都相等。比如,在一個初始的table中,放入(33,"a"),(17,"b")。他們的hash分別為33和17,key也分別為33和17。當(dāng)我調(diào)用get(17)時(shí),先會根據(jù)17算出在table中的索引為1,然后取出在這個索引中的第一個元素(33,"aa"),讓對比他們的hash和key是否都相等。顯而易見,第一個元素的key和hash都是33,而我們想要get的hash和key都是17.所以不相等。那么他就會去獲取第一個元素的next是否存在,如果存在會獲取出來在判斷hash和key是否都相等。
3. remove(Object key)方法:
和get(Object key)方法類似,先計(jì)算索引位置,找出這個索引位置的第一個Node命名為p,在對比 p的key,hash和參數(shù)中的key,根據(jù)參數(shù)key計(jì)算出來的hash是否一樣,如果一樣那么就在這個索引位置的值設(shè)為null。如果在有碰撞的情況下,就會與p.next做對比,如果一樣那么p.next將指向這個p.next.next。然后這個元素沒有了指針也會就被jvm回收了。
4.entrySet()方法:
我遍歷了一個HashMap看了看,因?yàn)橄肟纯此窃趺窗雅鲎驳耐粋€索引位置的那么多數(shù)取出了的,發(fā)現(xiàn)這個代碼不是很好理解,經(jīng)過百度和自己猜測,有了一點(diǎn)了解。當(dāng)時(shí)情況是這樣的:
這個在代碼中是這樣的:調(diào)用entrySet方法來遍歷出一個個Map.Entry
for(Map.Entry<? extends K,? extends V> e:m.entrySet()){
K key=e.getKey();
V value=e.getValue();
}entrySet()方法的代碼如下:
public Set<Map.Entry<K, V>> entrySet(){
Set<Map.Entry<K, V>> es;
return (es=entrySet)==null?(es=new EntrySet()):es;
}這個entrySet是等于null的,也就是說每次都是new EntrySet();
EntrySet類代碼
final class EntrySet extends AbstractSet<Map.Entry<K, V>>{
public final int size(){return size;}
public final void clear(){HashMapMmc.this.clear();}
public final Iterator<Map.Entry<K, V>> iterator(){
return new EntryIterator();
}
public final boolean contains(Object o){
if(!(o instanceof Map.Entry))
return false;
Map.Entry<?, ?> e=(Map.Entry<?, ?>) o;
Object key=e.getKey();
Node<K,V> candidate=getNode(hash(key),key);
return candidate!=null&&candidate.equals(o);
}
public final boolean remove(Object o){
if(o instanceof Map.Entry){
Map.Entry<?, ?> e=(java.util.Map.Entry<?, ?>) o;
Object key= e.getKey();
Object value=e.getValue();
return removeNode(hash(key), key, value, true,true)!=null;
}
return false;
}
public final Spliterator<Map.Entry<K, V>> spliterator(){
return new EntrySpliterator<>(HashMapMmc.this,0,-1,0,0);
}
public final void forEach(Consumer<? super Map.Entry<K, V>> action){
Node<K,V> [] tab;
if(action==null)
throw new NullPointerException();
if(size>0&&(tab=table)!=null){
int mc=modCount;
for(int i=0;i<tab.length;++i){
for(Node<K,V> e=tab[i];e!=null;e=e.next)
action.accept(e);
}
if(modCount!=mc)
throw new ConcurrentModificationException();
}
}
}看了EntrySet之后,感覺new EntrySet()里面不應(yīng)該是空的嗎?怎么能夠遍歷出值來呢?
但是debug了下下面的這個e確實(shí)是有值的。最后查找了一下資料得出,增強(qiáng)性for循環(huán)內(nèi)部是使用的iterator方法,又看了看果然EntrySet類中覆寫了iterator方法。返回的是一個new EntryIterator(),我又去找EntryIterator類,類里就只有一個方法。然后又發(fā)現(xiàn)它繼承了HashIterator類,
這個類東西就多了。
看下面的代碼:
for(Map.Entry<? extends K,? extends V> e:m.entrySet()){}abstract class HashIterator{
Node<K,V> next;
Node<K,V> current;
int expectedModeCount;
int index;
HashIterator(){
expectedModeCount=modCount;
Node<K,V>[] t=table;
current=next=null;
index=0;
if(t!=null&&size>0){ //先入先進(jìn)
do{}while(index<t.length&&(next=t[index++])==null);
}
}
public final boolean hasNext(){
return next!=null;
}
final Node<K,V> nextNode(){
Node<K,V>[] t;
Node<K,V> e= next;
if(modCount!=expectedModeCount)
throw new ConcurrentModificationException();
if(e==null)
throw new NoSuchElementException();
if((next=(current=e).next)==null&&(t=table)!=null){
do{}while(index<t.length&&(next=t[index++])==null);
}
return e;
}
public final void remove(){
Node<K,V> p=current;
if(p==null)
throw new IllegalStateException();
if(modCount!=expectedModeCount)
throw new ConcurrentModificationException();
current=null;
K key=p.key;
removeNode(hash(key),key,null,false,false);
expectedModeCount=modCount;
}
}可以看出這個HashIterator迭代器的默認(rèn)構(gòu)造器中,會初始化一個next的變量,這個變量是在table數(shù)組中取得,索引是從0遞增的,即先入先出原則。構(gòu)造初期會從0開始找有值的索引位置,找到后將這個Node賦值給next;然后要遍歷的時(shí)候是調(diào)用nextNode()方法,這個方法是先判斷next.next是否為空,如果為空繼續(xù)往上找有值的索引位置,如果不為空就找next.next。這樣就能都遍歷出來了,是從索引0到table.length去一個個尋找遍歷的。
第一次寫自己的理解,希望多多指正!
以上就是通過HashMap原理詳解entrySet中的疑問 的詳細(xì)內(nèi)容,更多關(guān)于HashMap entrySet疑問 的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于Intellij IDEA中的Version Control問題
這篇文章主要介紹了Intellij IDEA中的Version Control問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11
SpringCloud?Eureka應(yīng)用全面介紹
Eureka是Netflix開發(fā)的服務(wù)發(fā)現(xiàn)框架,本身是一個基于REST的服務(wù),主要用于定位運(yùn)行在AWS域中的中間層服務(wù),以達(dá)到負(fù)載均衡和中間層服務(wù)故障轉(zhuǎn)移的目的2022-09-09
@Controller、@RestController注解區(qū)別詳解
這篇文章主要介紹了@Controller、@RestController注解區(qū)別詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
源碼分析Spring?中?@Qualifier?注解基本用法
這篇文章主要介紹了源碼分析Spring?中?@Qualifier?注解基本用法,在源碼分析的過程中,也?GET?到?Spring?許多新的玩法,感興趣的小伙伴趕緊去試試吧2023-08-08
Windows系統(tǒng)下Java連接SQL Server的方法簡介
這篇文章主要介紹了Windows系統(tǒng)下Java連接SQL Server的方法,分別是JDBC和JTDS的相關(guān)使用,需要的朋友可以參考下2015-09-09
java使用監(jiān)聽器實(shí)現(xiàn)一個統(tǒng)計(jì)網(wǎng)站在線人數(shù)的示例
本文主要介紹了java使用監(jiān)聽器實(shí)現(xiàn)一個統(tǒng)計(jì)網(wǎng)站在線人數(shù)的示例,具有一定的參考價(jià)值,有需要的朋友可以了解一下。2016-10-10
Springboot內(nèi)置的工具類之CollectionUtils示例講解
這篇文章主要介紹了Springboot內(nèi)置的工具類之CollectionUtils,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
SpringBoot實(shí)現(xiàn)文件在線預(yù)覽功能的全過程
我們開發(fā)業(yè)務(wù)系統(tǒng)的時(shí)候,經(jīng)常有那種文檔文件在線預(yù)覽的需求,下面這篇文章主要給大家介紹了關(guān)于SpringBoot實(shí)現(xiàn)文件在線預(yù)覽功能的相關(guān)資料,需要的朋友可以參考下2021-11-11

