面試再問 HashMap,求你把這篇文章發(fā)給他!

數(shù)據(jù)結(jié)構(gòu) table數(shù)組長度永遠為2的冪次方 擴容 查找 插入 刪除 遍歷 equasl和hashcode 總結(jié)
默認大小、負載因子以及擴容倍數(shù) 底層數(shù)據(jù)結(jié)構(gòu) 如何處理hash沖突 如何計算key的hash值 數(shù)組長度為什么是2的冪次方 查找、插入、擴容過程 fail-fast機制
數(shù)據(jù)結(jié)構(gòu)
在 JDK1.8 中,HashMap 是由 數(shù)組+鏈表+紅黑樹構(gòu)成(1.7版本是數(shù)組+鏈表)當一個值中要存儲到HashMap中的時候會根據(jù)Key的值來計算出他的hash,通過hash值來確認存放到數(shù)組中的位置,如果發(fā)生hash沖突就以鏈表的形式存儲,當鏈表過長的話,HashMap會把這個鏈表轉(zhuǎn)換成紅黑樹來存儲,如圖所示:

//默認初始容量為16
static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<4;
//默認負載因子為0.75
static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;
//Hash數(shù)組(在resize()中初始化)
transient?Node[]?table;
//元素個數(shù)
transient?int?size;
//容量閾值(元素個數(shù)大于等于該值時會自動擴容)
int?threshold;
static?class?Node<K,V>?implements?Map.Entry<K,V>?{
????final?int?hash;
????final?K?key;
????V?value;
????Node?next;
????Node(int?hash,?K?key,?V?value,?Node?next)?{
????????this.hash?=?hash;
????????this.key?=?key;
????????this.value?=?value;
????????this.next?=?next;
????}
????public?final?K?getKey()????????{?return?key;?}
????public?final?V?getValue()??????{?return?value;?}
????public?final?String?toString()?{?return?key?+?"="?+?value;?}
????public?final?int?hashCode()?{
????????return?Objects.hashCode(key)?^?Objects.hashCode(value);//^表示相同返回0,不同返回1
????????//Objects.hashCode(o)————>return?o?!=?null???o.hashCode()?:?0;
????}
????public?final?V?setValue(V?newValue)?{
????????V?oldValue?=?value;
????????value?=?newValue;
????????return?oldValue;
????}
????public?final?boolean?equals(Object?o)?{
????????if?(o?==?this)
????????????return?true;
????????if?(o?instanceof?Map.Entry)?{
????????????Map.Entry,?>?e?=?(Map.Entry,?>)o;
????????????//Objects.equals(1,b)————>?return?(a?==?b)?||?(a?!=?null?&&?a.equals(b));
????????????if?(Objects.equals(key,?e.getKey())?&&?Objects.equals(value,?e.getValue()))
????????????????return?true;
????????}
????????return?false;
????}
}
默認初始容量為 16,默認負載因子為0.75threshold = 數(shù)組長度 * loadFactor,當元素個數(shù)大于等于threshold(容量閾值)時,HashMap會進行擴容操作table數(shù)組中存放指向鏈表的引用
table數(shù)組長度永遠為2的冪次方
tableSizeFor的方法來確保HashMap數(shù)組長度永遠為2的冪次方的,源碼如下:/*找到大于或等于?cap?的最小2的冪,用來做容量閾值*/
static?final?int?tableSizeFor(int?cap)?{
????int?n?=?cap?-?1;
????n?|=?n?>>>?1;
????n?|=?n?>>>?2;
????n?|=?n?>>>?4;
????n?|=?n?>>>?8;
????n?|=?n?>>>?16;
????return?(n?0)???1?:?(n?>=?MAXIMUM_CAPACITY)???MAXIMUM_CAPACITY?:?n?+?1;
}
cap-1再賦值給n的目的是另找到的目標值大于或等于原值。例如二進制1000,十進制數(shù)值為8。如果不對它減1而直接操作,將得到答案10000,即16。顯然不是結(jié)果。減1后二進制為111,再進行操作則會得到原來的數(shù)值1000,即8。通過一系列位運算大大提高效率。tableSizeFor方法呢?/*傳入初始容量和負載因子*/
public?HashMap(int?initialCapacity,?float?loadFactor)?{
????if?(initialCapacity?0)
????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+initialCapacity);
????if?(initialCapacity?>?MAXIMUM_CAPACITY)
????????initialCapacity?=?MAXIMUM_CAPACITY;
????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))
????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+loadFactor);
????this.loadFactor?=?loadFactor;
????this.threshold?=?tableSizeFor(initialCapacity);
}
那么為什么要把數(shù)組長度設(shè)計為2的冪次方呢?
index=hash&(table.length-1)這條公式來計算元素在table數(shù)組中存放的下標,就是把元素的hash值和數(shù)組長度減1的值做一個與運算,即可求出該元素在數(shù)組中的下標,這條公式其實等價于hash%length,也就是對數(shù)組長度求模取余,只不過只有當數(shù)組長度為2的冪次方時,hash&(length-1)才等價于hash%length,使用位運算可以提高效率。擴容
首先會判斷table數(shù)組長度,如果大于0說明已被初始化過,那么 按當前table數(shù)組長度的2倍進行擴容,閾值也變?yōu)樵瓉淼?倍若table數(shù)組未被初始化過,且threshold(閾值)大于0說明調(diào)用了 HashMap(initialCapacity, loadFactor)構(gòu)造方法,那么就把數(shù)組大小設(shè)為threshold若table數(shù)組未被初始化,且threshold為0說明調(diào)用 HashMap()構(gòu)造方法,那么就把數(shù)組大小設(shè)為16,threshold設(shè)為16*0.75接著需要判斷如果不是第一次初始化,那么擴容之后,要重新計算鍵值對的位置,并把它們移動到合適的位置上去,如果節(jié)點是紅黑樹類型的話則需要進行紅黑樹的拆分。
hash & oldCap的值來判斷,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長度,為什么呢?具體原因如下:
/*擴容*/
final?Node[]?resize()?{
????Node[]?oldTab?=?table;
????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;
????int?oldThr?=?threshold;
????int?newCap,?newThr?=?0;
????//1、若oldCap>0?說明hash數(shù)組table已被初始化
????if?(oldCap?>?0)?{
????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
????????????threshold?=?Integer.MAX_VALUE;
????????????return?oldTab;
????????}//按當前table數(shù)組長度的2倍進行擴容,閾值也變?yōu)樵瓉淼?倍
????????else?if?((newCap?=?oldCap?<1)?=?DEFAULT_INITIAL_CAPACITY)
????????????newThr?=?oldThr?<1;
????}//2、若數(shù)組未被初始化,而threshold>0說明調(diào)用了HashMap(initialCapacity)和HashMap(initialCapacity,?loadFactor)構(gòu)造器
????else?if?(oldThr?>?0)
????????newCap?=?oldThr;//新容量設(shè)為數(shù)組閾值
????else?{?//3、若table數(shù)組未被初始化,且threshold為0說明調(diào)用HashMap()構(gòu)造方法
????????newCap?=?DEFAULT_INITIAL_CAPACITY;//默認為16
????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);//16*0.75
????}
????//若計算過程中,閾值溢出歸零,則按閾值公式重新計算
????if?(newThr?==?0)?{
????????float?ft?=?(float)newCap?*?loadFactor;
????????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
??????????????????(int)ft?:?Integer.MAX_VALUE);
????}
????threshold?=?newThr;
????//創(chuàng)建新的hash數(shù)組,hash數(shù)組的初始化也是在這里完成的
????Node[]?newTab?=?(Node [])new?Node[newCap];
????table?=?newTab;
????//如果舊的hash數(shù)組不為空,則遍歷舊數(shù)組并映射到新的hash數(shù)組
????if?(oldTab?!=?null)?{
????????for?(int?j?=?0;?j?????????????Node?e;
????????????if?((e?=?oldTab[j])?!=?null)?{
????????????????oldTab[j]?=?null;//GC
????????????????if?(e.next?==?null)//如果只鏈接一個節(jié)點,重新計算并放入新數(shù)組
????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
????????????????//若是紅黑樹,則需要進行拆分
????????????????else?if?(e?instanceof?TreeNode)
????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);
????????????????else?{
????????????????????//rehash————>重新映射到新數(shù)組
????????????????????Node?loHead?=?null,?loTail?=?null;
????????????????????Node?hiHead?=?null,?hiTail?=?null;
????????????????????Node?next;
????????????????????do?{
????????????????????????next?=?e.next;
????????????????????????/*注意這里使用的是:e.hash & oldCap,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長度*/
????????????????????????if?((e.hash?&?oldCap)?==?0)?{
????????????????????????????if?(loTail?==?null)
????????????????????????????????loHead?=?e;
????????????????????????????else
????????????????????????????????loTail.next?=?e;
????????????????????????????loTail?=?e;
????????????????????????}
????????????????????????else?{
????????????????????????????if?(hiTail?==?null)
????????????????????????????????hiHead?=?e;
????????????????????????????else
????????????????????????????????hiTail.next?=?e;
????????????????????????????hiTail?=?e;
????????????????????????}
????????????????????}?while?((e?=?next)?!=?null);
????????????????????if?(loTail?!=?null)?{
????????????????????????loTail.next?=?null;
????????????????????????newTab[j]?=?loHead;
????????????????????}
????????????????????if?(hiTail?!=?null)?{
????????????????????????hiTail.next?=?null;
????????????????????????newTab[j?+?oldCap]?=?hiHead;
????????????????????}
????????????????}
????????????}
????????}
????}
????return?newTab;
}
鏈表樹化
鏈表長度大于等于8 table數(shù)組長度大于等于64
紅黑樹拆分
查找
首先通過自定義的hash方法計算出key的hash值,求出在數(shù)組中的位置 判斷該位置上是否有節(jié)點,若沒有則返回null,代表查詢不到指定的元素 若有則判斷該節(jié)點是不是要查找的元素,若是則返回該節(jié)點 若不是則判斷節(jié)點的類型,如果是紅黑樹的話,則調(diào)用紅黑樹的方法去查找元素 如果是鏈表類型,則遍歷鏈表調(diào)用equals方法去查找元素
hash方法計算哈希值,我們來看看其實現(xiàn):static?final?int?hash(Object?key)?{
????int?h;
????return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
}
(h = key.hashCode()) ^ (h >>> 16) 是為了讓高位數(shù)據(jù)與低位數(shù)據(jù)進行異或,變相的讓高位數(shù)據(jù)參與到計算中,int有 32 位,右移16位就能讓低16位和高16位進行異或,也是為了增加hash值的隨機性。get方法public?V?get(Object?key)?{
????Node?e;
????return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;//hash(key)不等于key.hashCode
}
final?Node?getNode(int?hash,?Object?key)? {
????Node[]?tab;?//指向hash數(shù)組
????Node?first,?e;?//first指向hash數(shù)組鏈接的第一個節(jié)點,e指向下一個節(jié)點
????int?n;//hash數(shù)組長度
????K?k;
????/*(n?-?1)?&?hash?————>根據(jù)hash值計算出在數(shù)組中的索引index(相當于對數(shù)組長度取模,這里用位運算進行了優(yōu)化)*/
????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(first?=?tab[(n?-?1)?&?hash])?!=?null)?{
????????//基本類型用==比較,其它用equals比較
????????if?(first.hash?==?hash?&&?((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????return?first;
????????if?((e?=?first.next)?!=?null)?{
????????????//如果first是TreeNode類型,則調(diào)用紅黑樹查找方法
????????????if?(first?instanceof?TreeNode)
????????????????return?((TreeNode)first).getTreeNode(hash,?key);
????????????do?{//向后遍歷
????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????????return?e;
????????????}?while?((e?=?e.next)?!=?null);
????????}
????}
????return?null;
}
(n - 1) & hash 計算key所對應的索引index(相當于對數(shù)組長度取模,這里用位運算進行了優(yōu)化),這點在上面已經(jīng)說過了,就不再廢話了。插入
當table數(shù)組為空時,通過擴容的方式初始化table 通過計算鍵的hash值求出下標后,若該位置上沒有元素(沒有發(fā)生hash沖突),則新建Node節(jié)點插入 若發(fā)生了hash沖突,遍歷鏈表查找要插入的key是否已經(jīng)存在,存在的話根據(jù)條件判斷是否用新值替換舊值 如果不存在,則將元素插入鏈表尾部,并根據(jù)鏈表長度決定是否將鏈表轉(zhuǎn)為紅黑樹 判斷鍵值對數(shù)量是否大于等于閾值,如果是的話則進行擴容操作
public?V?put(K?key,?V?value)?{
????return?putVal(hash(key),?key,?value,?false,?true);
}
final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,boolean?evict)?{
????Node[]?tab;//指向hash數(shù)組
????Node?p;//初始化為table中第一個節(jié)點
????int?n,?i;//n為數(shù)組長度,i為索引
????//tab被延遲到插入新數(shù)據(jù)時再進行初始化
????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????????n?=?(tab?=?resize()).length;
????//如果數(shù)組中不包含Node引用,則新建Node節(jié)點存入數(shù)組中即可
????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????tab[i]?=?newNode(hash,?key,?value,?null);//new?Node<>(hash,?key,?value,?next)
????else?{
????????Node?e;?//如果要插入的key-value已存在,用e指向該節(jié)點
????????K?k;
????????//如果第一個節(jié)點就是要插入的key-value,則讓e指向第一個節(jié)點(p在這里指向第一個節(jié)點)
????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????e?=?p;
????????//如果p是TreeNode類型,則調(diào)用紅黑樹的插入操作(注意:TreeNode是Node的子類)
????????else?if?(p?instanceof?TreeNode)
????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);
????????else?{
????????????//對鏈表進行遍歷,并用binCount統(tǒng)計鏈表長度
????????????for?(int?binCount?=?0;?;?++binCount)?{
????????????????//如果鏈表中不包含要插入的key-value,則將其插入到鏈表尾部
????????????????if?((e?=?p.next)?==?null)?{
????????????????????p.next?=?newNode(hash,?key,?value,?null);
????????????????????//如果鏈表長度大于或等于樹化閾值,則進行樹化操作
????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)
????????????????????????treeifyBin(tab,?hash);
????????????????????break;
????????????????}
????????????????//如果要插入的key-value已存在則終止遍歷,否則向后遍歷
????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????????break;
????????????????p?=?e;
????????????}
????????}
????????//如果e不為null說明要插入的key-value已存在
????????if?(e?!=?null)?{
????????????V?oldValue?=?e.value;
????????????//根據(jù)傳入的onlyIfAbsent判斷是否要更新舊值
????????????if?(!onlyIfAbsent?||?oldValue?==?null)
????????????????e.value?=?value;
????????????afterNodeAccess(e);
????????????return?oldValue;
????????}
????}
????++modCount;
????//鍵值對數(shù)量大于等于閾值時,則進行擴容
????if?(++size?>?threshold)
????????resize();
????afterNodeInsertion(evict);//也是空函數(shù)?回調(diào)?不知道干嘛的
????return?null;
}
刪除
定位桶位置 遍歷鏈表找到相等的節(jié)點 第三步刪除節(jié)點
public?V?remove(Object?key)?{
????Node?e;
????return?(e?=?removeNode(hash(key),?key,?null,?false,?true))?==?null???null?:?e.value;
}
final?Node?removeNode(int?hash,?Object?key,?Object?value,boolean?matchValue,?boolean?movable)? {
????Node[]?tab;
????Node?p;
????int?n,?index;
????//1、定位元素桶位置
????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(p?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{
????????Node?node?=?null,?e;
????????K?k;
????????V?v;
????????//?如果鍵的值與鏈表第一個節(jié)點相等,則將?node?指向該節(jié)點
????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????node?=?p;
????????else?if?((e?=?p.next)?!=?null)?{
????????????//?如果是?TreeNode?類型,調(diào)用紅黑樹的查找邏輯定位待刪除節(jié)點
????????????if?(p?instanceof?TreeNode)
????????????????node?=?((TreeNode)p).getTreeNode(hash,?key);
????????????else?{
????????????????//?2、遍歷鏈表,找到待刪除節(jié)點
????????????????do?{
????????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))?{
????????????????????????node?=?e;
????????????????????????break;
????????????????????}
????????????????????p?=?e;
????????????????}?while?((e?=?e.next)?!=?null);
????????????}
????????}
????????//?3、刪除節(jié)點,并修復鏈表或紅黑樹
????????if?(node?!=?null?&&?(!matchValue?||?(v?=?node.value)?==?value?||?(value?!=?null?&&?value.equals(v))))?{
????????????if?(node?instanceof?TreeNode)
????????????????((TreeNode)node).removeTreeNode(this,?tab,?movable);
????????????else?if?(node?==?p)
????????????????tab[index]?=?node.next;
????????????else
????????????????p.next?=?node.next;
????????????++modCount;
????????????--size;
????????????afterNodeRemoval(node);
????????????return?node;
????????}
????}
????return?null;
}
遍歷
????Map ?map?=?new?HashMap<>();
????????map.put("1",?1);
????????map.put("2",?2);
????????map.put("3",?3);
????????for?(String?s?:?map.keySet())?{
????????????if?(s.equals("2"))
????????????????map.remove("2");
????????}
transient?int?modCount;
public?Set ?keySet()? {
????Set?ks?=?keySet;
????if?(ks?==?null)?{
????????ks?=?new?KeySet();
????????keySet?=?ks;
????}
????return?ks;
}
final?class?KeySet?extends?AbstractSet<K>?{
????public?final?Iterator?iterator()????? {?return?new?KeyIterator();?}
????//?省略部分代碼
}
final?class?KeyIterator?extends?HashIterator?implements?Iterator<K>?{
????public?final?K?next()?{?return?nextNode().key;?}
}
/*HashMap迭代器基類,子類有KeyIterator、ValueIterator等*/
abstract?class?HashIterator?{
????Node?next;????????//下一個節(jié)點
????Node?current;?????//當前節(jié)點
????int?expectedModCount;??//修改次數(shù)
????int?index;?????????????//當前索引
????//無參構(gòu)造
????HashIterator()?{
????????expectedModCount?=?modCount;
????????Node[]?t?=?table;
????????current?=?next?=?null;
????????index?=?0;
????????//找到第一個不為空的桶的索引
????????if?(t?!=?null?&&?size?>?0)?{
????????????do?{}?while?(index?null);
????????}
????}
????//是否有下一個節(jié)點
????public?final?boolean?hasNext()?{
????????return?next?!=?null;
????}
????//返回下一個節(jié)點
????final?Node?nextNode()? {
????????Node[]?t;
????????Node?e?=?next;
????????if?(modCount?!=?expectedModCount)
????????????throw?new?ConcurrentModificationException();//fail-fast
????????if?(e?==?null)
????????????throw?new?NoSuchElementException();
????????//當前的鏈表遍歷完了就開始遍歷下一個鏈表
????????if?((next?=?(current?=?e).next)?==?null?&&?(t?=?table)?!=?null)?{
????????????do?{}?while?(index?null);
????????}
????????return?e;
????}
????//刪除元素
????public?final?void?remove()?{
????????Node?p?=?current;
????????if?(p?==?null)
????????????throw?new?IllegalStateException();
????????if?(modCount?!=?expectedModCount)
????????????throw?new?ConcurrentModificationException();
????????current?=?null;
????????K?key?=?p.key;
????????removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode
????????expectedModCount?=?modCount;
????}
}
if?(modCount?!=?expectedModCount)
????????????throw?new?ConcurrentModificationException();
removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode
expectedModCount?=?modCount;
Map ?map?=?new?HashMap<>();
????????map.put("1",?1);
????????map.put("2",?2);
????????map.put("3",?3);
????????Iterator?iterator?=?map.keySet().iterator();
????????while?(iterator.hasNext()){
????????????if?(iterator.next().equals("2"))
????????????????iterator.remove();
????????}

equasl和hashcode
public?class?Person?{
????Integer?id;
????String?name;
????public?Person(Integer?id,?String?name)?{
????????this.id?=?id;
????????this.name?=?name;
????}
????@Override
????public?boolean?equals(Object?obj)?{
????????if?(obj?==?null)?return?false;
????????if?(obj?==?this)?return?true;
????????if?(obj?instanceof?Person)?{
????????????Person?person?=?(Person)?obj;
????????????if?(this.id?==?person.id)
????????????????return?true;
????????}
????????return?false;
????}
????public?static?void?main(String[]?args)?{
????????Person?p1?=?new?Person(1,?"aaa");
????????Person?p2?=?new?Person(1,?"bbb");
????????HashMap?map?=?new?HashMap<>();
????????map.put(p1,?"這是p1");
????????System.out.println(map.get(p2));
????}
}
原生的equals方法是使用==來比較對象的 原生的hashCode值是根據(jù)內(nèi)存地址換算出來的一個值
總結(jié)
最近熱文
? ?思科前員工刪庫跑路,損失達 1600 多萬 ???如何破解“僅三天可見”的朋友圈? ???面試官寫了個雙冒號::問我這是什么語法?Java中有這玩意? ???Java反射到底慢在哪? 最近整理了一份大廠算法刷題指南,包括一些刷題技巧,在知乎上已經(jīng)有上萬贊。同時還整理了一份6000頁面試筆記。關(guān)注下面公眾號,在公眾號內(nèi)回復「刷題」,即可免費獲??!回復「加群」,可以邀請你加入讀者群!
明天見(??ω??)??
評論
圖片
表情
