HashMap是求職面試中名副其實的“明星”,基本上每一加公司的面試多多少少都有問到HashMap的底層實作原理、源碼等相關問題,這篇文章将會按以下順序來組織:
- HashMap源碼分析(JDK8,通俗易懂)
- HashMap面試“明星”問題彙總,以及明星問題答案
HashMap的成員屬性源碼分析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
/**
* The default initial capacity - MUST be a power of two.
*/
//HashMap的初始容量為16,HashMap的容量指的是存儲元素的數組大小,即桶的數量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
//HashMap的最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
/**
* DEFAULT_LOAD_FACTOR:HashMap的負載因子,影響HashMap性能的參數之一,是時間和空間之間的權
* 衡,後面會看到HashMap的元素存儲在Node數組中,這個數組的大小這裡稱為“桶”的大小.
* 另外還有一個參數size指的是我們往HashMap中put了多少個元素。
* 當size==桶的數量*DEFAULT_LOAD_FACTOR的時候,這時HashMap要進行擴容操作,也就是桶不能裝
* 滿。DEFAULT_LOAD_FACTOR是衡量桶的使用率:DEFAULT_LOAD_FACTOR較小時(桶的使用率較小),
* 這時浪費的空間較多(因為隻能存儲桶的數量*DEFAULT_LOAD_FACTOR個元素,超過了就要進行擴容),
* 這種情況下往HashMap中put元素時發生沖突的機率也很小,所謂沖突指的是:多個元素被put到了同一個桶 中;
* 沖突小時(可以認為一個桶中隻有一個元素)put、get等HashMap的操作代價就很低,可以認為是O(1);
* 桶的使用率較大的時候(DEFAULT_LOAD_FACTOR很大,注意可以大于1,因為沖突的元素是使用連結清單或者 紅黑樹連接配接起來的)
* 空間使用率較高,
* 這也意味着一個桶中存儲了很多元素,這時HashMap的put、get等操作代價就相對較大,因為每一個put或get操作都變成了
* 對連結清單或者紅黑樹的操作,代價肯定大于O(1),是以說DEFAULT_LOAD_FACTOR是空間和時間的一個平衡點;
* DEFAULT_LOAD_FACTOR較小時,需要的空間較大,但是put和get的代價較小;
* DEFAULT_LOAD_FACTOR較大時,需要的空間較小,但是put和get的代價較大)。
*
* 擴容操作就是把桶的數量*2,即把Node數組的大小調整為擴容前的2倍,至于為什麼是兩倍,分析擴容函 數時會講解,
* 這其實是一個trick。Node數組中每一個桶中存儲的是Node連結清單,當連結清單長度>=8的時候,
* 連結清單會變為紅黑樹結構(因為紅黑樹的增删改查複雜度是logn,連結清單是n,紅黑樹結構比連結清單代價更小)。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
//當某一個桶中連結清單的長度>=8時,連結清單結構會轉換成紅黑樹結構(其實還要求桶的中數量>=64,後面會提到)
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
//當紅黑樹中的節點數量<=6時,紅黑樹結構會轉變為連結清單結構
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
//上面提到的:當Node數組容量>=64的前提下,如果某一個桶中連結清單長度>=8,則會将連結清單結構轉換成
//紅黑樹結構
static final int MIN_TREEIFY_CAPACITY = 64;
}
HashMap内部類——Node源碼分析
下面介紹HashMap的内部類Node,HashMap的所有資料都儲存在Node數組中那麼這個Node到底是個什麼東西呢?
//Node是HashMap的内部類
static class Node<K,V> implements Map.Entry<K,V> {
//儲存key的hashcode=key的hashcode ^ (key的hashcode>>>16)這樣做主要是為了減少hash沖突
//當我們往map中put(k,v)時,這個k,v鍵值對會被封裝為Node,那麼這個Node放在Node數組的哪個
//位置呢:index=hash&(n-1),n為Node數組的長度。那為什麼這樣計算hash可以減少沖突呢?如果直接
//使用hashCode&(n-1)來計算index,此時hashCode的高位随機特性完全沒有用到,因為n相對于
//hashcode的值很小。基于這一點,把hashcode 高16位的值通過異或混合到hashCode的低16位,由此
//來增強hashCode低16位的随機性
final int hash;
final K key;//儲存map中的key
V value;//儲存map中的value
Node<K,V> next;//單向連結清單
Node(int hash, K key, V value, Node<K,V> next) {//構造器
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
HashMap hash函數和tableSizeFor函數源碼分析(這兩個函數後面會用到)
/**
* HashMap允許key為null,null的hash為0(也意味着HashMap允許key為null的鍵值對),
* 非null的key的hash高16位和低16位分别由由:key的hashCode
* 高16位和hashCode的高16位異或hashCode的低16位組成。主要是為了增強hash的随機性減少hash&(n-1)的
* 随機性,即減小hash沖突,提高HashMap的性能。是以作為HashMap的key的hashCode函數的實作對HashMap
* 的性能影響較大,極端情況下:所有key的hashCode都相同,這是HashMap的性能很糟糕!
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 在new HashMap的時候,如果我們傳入了大小參數,這是HashMap會對我們傳入的HashMap容量進行傳到
* tableSizeFor函數處理:這個函數主要功能是:傳回一個數:這個數是大于等于cap并且是2的整數次幂
* 的所有數中最小的那個,即傳回一個最接近cap(>=cap),并且是2的整數次幂的那個數。
* 具體邏輯如下:一個數是2的整數次幂,那麼這個數減1的二進制就是一串掩碼,即二進制從某位開始是一 串連續的1。
*/
static final int tableSizeFor(int cap) {
//舉例而言:n的第三位是1(從高位開始數),
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//舉例而言:如果n為: 00010000 00000000 00000000 000
/*
n |= n >>> 1;->n:00011000 00000000 00000000 0000
n |= n >>> 2;->n: 00011110 00000000 00000000 0000
n |= n >>> 4;->n: 00011111 11100000 00000000 0000
n |= n >>> 8;->n: 00011111 11111111 11100000 0000
n |= n >>> 16;->n:00011111 11111111 11111111 1111
傳回n+1:00010000 00000000 00000000 000(>=cap并且為2的整數次幂,與cap內插補點最小的那個)
最後的n+1一定是2的整數次幂,并且一定是>=cap
整體的思路就是:如果n二進制的第k為1,那麼經過上面四個‘|’運算後[0,k]位都變成了1,
即:一連串連續的二進制‘1’(掩碼),最後n+1一定是2的整數次幂(如果不溢出)
*/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap成員屬性源碼分析
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
//我們往map中put的(k,v)都被封裝在Node中,所有的Node都存放在table數組中
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
//用于傳回keySet和values
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
//儲存map目前有多少個元素
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
//failFast機制,在講解ArrayList和LinkedList一文中已經講過了
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
//這也是比較重要的一個屬性:
//建立HashMap時,改變量的值是:初始容量(2的整數次幂)
//之後threshold的值是HashMap擴容的門限值:即目前Node/table數組的長度*loadfactor
//舉個例子而言,如果我們傳給HashMap構造器的容量大小為9,那麼threshold初始值為16,在向HashMap中
//put第一個元素後,内部會建立長度為16的Node數組,并且threshold的值更新為16*0.75=12。
//具體而言,當我們一直往HashMap put元素的時候,,如果put某個元素後,Node數組中元素個數為13了
//,此時會觸發擴容(因為數組中元素個數>threshold了,即13>threshold=12),具體擴容操作之後會
//詳細分析,簡單了解就是,擴容操作将Node數組長度*2;并且将原來的所有元素遷移到新的Node數組中。
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
//負載因子,見上面對DEFAULT_LOAD_FACTOR 參數的講解,預設值是0.75
final float loadFactor;
HashMap構造器源碼分析
//構造器:指定map的大小,和loadfactor
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;//儲存loadfactor
//注意,前面有講tableSizeFor函數,該函數傳回值:>=initialCapacity、傳回值是2的整數次幂
//并且得是滿足上面兩個條件的所有數值中最小的那個數
this.threshold = tableSizeFor(initialCapacity);
}
//隻指定HashMap容量的構造器,loadfactor使用的是預設的值:0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//無參構造器,預設loadfactor:0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//其他不常用的構造器就不分析了
從構造器中我們可以看到:HashMap是“懶加載”,在構造器中值保留了相關保留的值,并沒有初始化table數組,當我們向map中put第一個元素的時候,map才會進行初始化!
HashMap的get函數源碼分析
//入口,傳回對應的value
public V get(Object key) {
Node<K,V> e;
//hash:這個函數上面分析過了。傳回key混淆後的hashCode
//注意getNode傳回的類型是Node:當傳回值為null時表示map中沒有對應的key,注意區分value為
//null:如果key對應的value為null的話,展現在getNode的傳回值e.value為null,此時傳回值也是
//null,也就是HashMap的get函數不能判斷map中是否有對應的key:get傳回值為null時,可能不包
//含該key,也可能該key的value為null!那麼如何判斷map中是否包含某個key呢?見下面contains
//函數分析
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
public boolean containsKey(Object key) {
//注意與get函數區分,我們往map中put的所有的<key,value>都被封裝在Node中,如果Node都不存在
//顯然一定不包含對應的key
return getNode(hash(key), key) != null;
}
//下面分析getNode函數
/**
* Implements Map.get and related methods
*
* @param hash hash for key //下面簡稱:key的hash值
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//(n-1)&hash:目前key可能在的桶索引,put操作時也是将Node存放在index=(n-1)&hash位置
//主要邏輯:如果table[index]處節點的key就是要找的key則直接傳回該節點;
//否則:如果在table[index]位置進行搜尋,搜尋是否存在目标key的Node:這裡的搜尋又分兩種:
//連結清單搜尋和紅黑樹搜尋,具體紅黑樹的查找就不展開了,紅黑樹是一種弱平衡(相對于AVL)BST,
//紅黑樹查找、删除、插入等操作都能夠保證在O(lon(n))時間複雜度内完成,紅黑樹原理不在本文
//範圍内,但是大家要知道紅黑樹的各種操作是可以實作的,簡單點可以把紅黑樹了解為BST,
//BST的查找、插入、删除等操作的實作在之前的部落格中有java實作代碼,實際上紅黑樹就是一種平
//衡的BST
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;//一次就比對到了,直接傳回,否則進行搜尋
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//紅黑樹搜尋/查找
return ((TreeNode<K,V>)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;//沒找到,傳回null
}
get函數實質就是進行連結清單或者紅黑樹周遊搜尋指定key的節點的過程;另外需要注意到HashMap的get函數的傳回值不能判斷一個key是否包含在map中,get傳回null有可能是不包含該key;也有可能該key對應的value為null。HashMap中允許key為null,允許value為null
put函數源碼解析
//put函數入口,兩個參數:key和value
public V put(K key, V value) {
/*下面分析這個函數,注意前3個參數,後面
2個參數這裡不太重要,因為所有的put
操作後面的2個參數預設值都一樣 */
return putVal(hash(key), key, value, false, true);
}
//下面是put函數的核心處理函數
final V putVal(int hash, K key, V value , boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p;
int n, i;
/*上面提到過HashMap是懶加載,所有put的時候要先檢查table數組是否已經
初始化了,沒有初始化得先初始化table數組,保證table數組一定初始化了 */
if ((tab = table) == null || (n = tab.length) == 0)
//這個函數後面有resize函數分析
n = (tab = resize()).length;
/*到這裡表示table數組一定初始化了與上面get函數相同,指定key的Node,會存儲
在table數組的i=(n-1)&hash 下标位置,get的時候也是從table數組的該位置搜尋 */
if ((p = tab[i = (n - 1) & hash])== null)
/*如果i位置還沒有存儲元素,則把目前的key,value封裝為Node,存儲在table[i]位置 */
tab[i] = newNode(hash, key, value, null);
else {
/*
如果table[i]位置已經有元素了,則接下來的流程是:
首先判斷連結清單或者二叉樹中是否已經存在key的鍵值對?
存在的話就更新它的value;
不存在的話把目前的key,value插入到連結清單的末尾或者插入到紅黑樹中
如果連結清單或者紅黑樹中已經存在Node.key等于key,
則e指向該Node,即e指向一個Node:
該Node的key屬性與put時傳入的key參數相等的那個Node,
後面會更新e.value
*/
Node<K,V> e; K k;
/*
為什麼get和put先判斷p.hash==hash,下面的if條件中去掉hash的比較邏輯
也是正确?因為hash的比較是兩個整數的比較,比較的代價相對較小,key是泛型,
對象的比較比整數比較代價大,是以先比較hash,hash相等再比較key
*/
if(p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
/*
e指向一個Node:該Node的key
屬性與put時傳入的key參數相等
的那個Node
*/
e = p;
else if (p instanceof TreeNode)
/*
紅黑樹的插入操作,如果已經存在該key的TreeNode,則傳回該TreeNode,否則傳回null
*/
e = ((TreeNode<K,V>)p)
.putTreeVal(this
, tab, hash, key, value);
else {
/*
table[i]處存放的是連結清單,接下來和TreeNode類似在周遊連結清單過程中先判斷
目前的key是否已經存在,如果存在則令e指向該Node;否則将該Node插入到鍊
表末尾,插入後判斷連結清單長度是否>=8,是的話要進行額外操作
*/
//binCountt最後的值是連結清單的長度
for (int binCount = 0; ;++binCount) {
if ((e = p.next) == null) {
/*
周遊到了連結清單最後一個元素,接下來執行連結清單的插入操作,先封裝為Node,
再插入p指向的是連結清單最後一個節點,将待插入的Node置為p.next,就完成了單連結清單的插入
*/
p.next = newNode(hash, key , value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
/*
TREEIFY_THRESHOLD值是8, binCount>=7,然後又插入了一個新節
點,連結清單長度>=8,這時要麼進行擴容操作,要麼把連結清單結構轉為紅黑樹結構。
我們接下會分析treeifyBin的源碼實作
*/
treeifyBin(tab, hash);
break;
}
/*
當p不是指向連結清單末尾的時候:先判斷p.key是否等于key,等于的話表示
目前key已經存在了,令e指向p,停止周遊,最後會更新e的value;
不等的話準備下次周遊,令p=p.next,即p=e。
*/
if (e.hash == hash && ((k = e.key) == key || (key != null&& key.equals(k))))
break;
p = e;
}
}
if (e != null) {
/*
表示目前的key在put之前已經存在了,并且上面的邏輯保證:
e已經指向了之前已經存在的Node,這時更新e.value就好。
*/
//更新oldvalue
V oldValue = e.value;
/*
onlyIfAbsent默是false, evict為true。onlyIfAbsent為true表示:
如果之前已經存在key這個鍵值對了,那麼後面再put這個key時,忽略這個操作,不更新先前的value。
這裡了解就好
*/
if (!onlyIfAbsent || oldValue == null)
//更新e.value
e.value = value;
/*
這個函數的預設實作是“空”,即這個函數預設什麼操作都
不執行,那為什麼要有它呢?這其實是個hook/鈎子函數,
主要要在LinkedHashMap(HashMap子類)中使用,
LinkedHashMap重寫了這 個函數。以後會有講解
LinkedHashMap的文章。
*/
afterNodeAccess(e);
//傳回舊的value
return oldValue;
}
}
//如果是第一次插入key這個鍵,
//就會執行到這裡
++modCount;//failFast機制
/*
size儲存的是目前HashMap中儲存
了多少個鍵值對,HashMap的size
方法就是直接傳回size之前說過,
threshold儲存的是目前table數
組長度*loadfactor,如果table
數組中存儲的Node數量大于
threshold,這時候會進行擴容,
即将table數組的容量翻倍。
後面會詳細講解resize方法。
*/
if (++size > threshold)
resize();
//這也是一個hook函數,作用和
//afterNodeAccess一樣
afterNodeInsertion(evict);
return null;
}
}
treeifyBin源碼解析
//将連結清單轉換為紅黑樹結構,在連結清單的
//插入操作後調用
final void treeifyBin (Node<K,V>[] tab , int hash) {
int n, index;
Node<K,V> e;
/*MIN_TREEIFY_CAPACITY值
是64,也就是當連結清單長度>8的
時候,有兩種情況:如果table
數組的長度<64,此時進行擴容
操作;如果table數組的長度>64,
此時進行連結清單轉紅黑樹結構的操作.
具體轉細節在面試中幾乎沒有問的,
這裡不細講了,大部同學認為連結清單長度
>8一定會轉換成紅黑樹,這是不對的!
*/
if (tab == null || (n = tab.length)< MIN_TREEIFY_CAPACITY)
resize();
else if ((e=tab[index=(n-1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p =replacementTreeNode(e , null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
HashMap的resize函數源碼分析
重點中的重點,面試談到HashMap必考resize相關知識,整體思路介紹:
有兩種情況會調用目前函數:
- 之前說過HashMap是懶加載,第一次hHashMap的put方法的時候table還沒初始化,這個時候會執行resize,進行table數組的初始化,table數組的初始容量儲存在threshold中(如果從構造器中傳入的一個初始容量的話),如果建立HashMap的時候沒有指定容量,那麼table數組的初始容量是預設值:16。即,初始化table數組的時候會執行resize函數
- 擴容的時候會執行resize函數,當size的值>threshold的時候會觸發擴容,即執行resize方法,這時table數組的大小會翻倍。
注意我們每次擴容之後容量都是翻倍( *2),是以HashMap的容量一定是2的整數次幂,那麼HashMap的容量為什麼一定得是2的整數次幂呢?(面試重點)。
要知道原因,首先回顧我們put key的時候,每一個key會對應到一個桶裡面,桶的索引是這樣計算的: index = hash & (n-1),index的計算最為直覺的想法是:hash%n,即通過取餘的方式把目前的key、value鍵值對散列到各個桶中;那麼這裡為什麼不用取餘(%)的方式呢?
原因是CPU對位運算支援較好,即位運算速度很快。另外,當n是2的整數次幂時:hash&(n-1)與hash%(n-1)是等價的,但是兩者效率來講是不同的,位運算的效率遠高于%運算。
基于上面的原因,HashMap中使用的是hash&(n-1)。這還帶來了一個好處,就是将舊數組中的Node遷移到擴容後的新數組中的時候有一個很友善的特性:
HashMap使用table數組儲存Node節點,是以table數組擴容的時候(數組擴容一定得是先重新開辟一個數組,然後把就數組中的元素重新散列(rehash)到新數組中去。
這裡舉一個例子來來說明這個特性:下面以Hash初始容量n=16,預設loadfactor=0.75舉例(其他2的整數次幂的容量也是類似的),預設容量:n=16,二進制:10000;n-1:15,n-1二進制:01111。某個時刻,map中元素大于16*0.75=12,即size>12。此時會發生擴容,即會建立了一個數組,容量為擴容前的兩倍,newtab,len=32。
接下來我們需要把table中的Node搬移(rehash)到newtab。從table的i=0位置開始處理,假設我們目前要處理table數組i索引位置的node,那這個node應該放在newtab的那個位置呢?下面的hash表示node.key對應的hash值,也就等于node.hash屬性值,另外為了簡單,下面的hash隻寫出了8位(省略的高位的0),實際上hash是32位:node在newtab中的索引:
index = hash % len=hash & (len-1)=hash & (32 - 1)=hash & 31=hash & (0x0001_1111);
再看node在table數組中的索引計算:
i = hash & (16 - 1) = hash & 15= hash & (0x0000_1111)。
注意觀察兩者的異同:
i = hash&(0x0000_1111);
index = hash&(0x0001_1111)
上面表達式有個特點:
index = hash & (0x0001_1111)
= hash & (0x0000_1111) | hash & (0x0001_0000)
= hash & (0x0000_1111) | hash & n)
= i + ( hash & n)
什麼意思呢:
hash&n要麼等于n要麼等于0;也就是:inde要麼等于i,要麼等于i+n;再具體一點:當hash&n==0的時候,index=i;
當hash&n==n的時候,index=i+n;這有什麼用呢?當我們把table[i]位置的所有Node遷移到newtab中去的時候:
這裡面的node要麼在newtab的i位置(不變),要麼在newtab的i+n位置;也就是我們可以這樣處理:把table[i]這個桶中的node拆分為兩個連結清單l1和類:如果hash&n==0,那麼目前這個node被連接配接到l1連結清單;否則連接配接到l2連結清單。這樣下來,當周遊完table[i]處的所有node的時候,我們得到兩個連結清單l1和l2,這時我們令newtab[i]=l1,newtab[i+n]=l2,這就完成了table[i]位置所有node的遷移/rehash,這也是HashMap中容量一定的是2的整數次幂帶來的友善之處。
下面的resize的邏輯就是上面講的那樣。将table[i]處的Node拆分為兩個連結清單,這兩個連結清單再放到newtab[i]和newtab[i+n]位置.
resize方法源碼解析
final Node<K,V>[] resize() {
//保留擴容前數組引用
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//正常擴容:newCap = oldCap << 1
else if ((newCap = oldCap << 1)< MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//容量翻倍,擴容後的threshold
//自然也是*2
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
// zero initial threshold
//signifies using defaults
//table數組初始化的時候會進入到這裡
//預設容量
newCap = DEFAULT_INITIAL_CAPACITY;
//threshold
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap*loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//更新threshold
@SuppressWarnings({"rawtypes","unchecked"})
//擴容後的新數組
Node<K,V>[] newTab = (Node<K,V>[]) new Node[newCap];
table = newTab;//執行容量翻倍的新數組
if (oldTab != null) {
//之後完成oldTab中Node遷移到table中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
/*j這個桶位置隻有一個元素,直接rehash到table數組 */
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/*如果是紅黑樹:也是将紅黑樹拆分為 兩個連結清單,這裡主要看連結清單的拆分,
兩者邏輯一樣*/
((TreeNode<K,V>)e).split( this, newTab, j, oldCap);
else {
//連結清單的拆分
//第一個連結清單l1
Node<K,V> loHead = null , loTail = null;
//第二個連結清單l2
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
/*rehash到table[j]位置
将目前node連接配接到l1上 */
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//将目前node連接配接到l2上
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//l1放到table[j]位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//l1放到table[j+oldCap]位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap面試“明星”問題彙總,及答案
你知道HashMap嗎,請你講講HashMap?
這個問題不單單考察你對HashMap的掌握程度,也考察你的表達、組織問題的能力。個人認為應該從以下幾個角度入手(所有常見HashMap的考點問題總結):
- size必須是2的整數次方原因
- get和put方法流程
- resize方法
- 影響HashMap的性能因素(key的hashCode函數實作、loadFactor、初始容量)
- HashMap key的hash值計算方法以及原因(見上面hash函數的分析)
- HashMap内部存儲結構:Node數組+連結清單或紅黑樹
- table[i]位置的連結清單什麼時候會轉變成紅黑樹(上面源碼中有講)
- HashMap主要成員屬性:threshold、loadFactor、HashMap的懶加載
- HashMap的get方法能否判斷某個元素是否在map中
- HashMap線程安全嗎,哪些環節最有可能出問題,為什麼?
- HashMap的value允許為null,但是HashTable和ConcurrentHashMap的valued都不允許為null,試分析原因?
- HashMap中的hook函數(在後面講解LinkedHashMap時會講到,這也是面試時拓展的一個點)
上面問題的答案都可以在上面的源碼分析中找到,下面在給三點補充:
HashMap的初始容量是怎樣影響HashMap的性能的?
假如你預先知道最多往HashMap中存儲64個元素,那麼你在建立HashMap的時候:如果選用無參構造器:預設容量16,在存儲16loadFactor個元素之後就要進行擴容(數組擴容涉及到連續空間的配置設定,Node節點的rehash,代價很高,是以要盡量避免擴容操作);如果給構造器傳入的參數是64,這時HashMap中在存儲64loadFactor個元素之後就要進行擴容;但是如果你給構造器傳的參數為:(int)(64/0.75)+1,此時就可以保證HashMap不用進行擴容,避免了擴容時的代價。
HashMap線程安全嗎,哪些環節最有可能出問題,為什麼?
我們都知道HashMap線程不安全,那麼哪些環節最優可能出問題呢,及其原因:沒有參照這個問題有點不好直接回答,但是我們可以找參照啊,參照:ConcurrentHashMap,因為大家都知道HashMap不是線程安全的,ConcurrentHashMap是線程安全的,對照ConcurrentHashMap,看看ConcurrentHashMap在HashMap的基礎之上增加了哪些安全措施,這個問題就迎刃而解了。後面會有分析ConcurrentHashMap的文章,這裡先簡要回答這個問題:HashMap的put操作是不安全的,因為沒有使用任何鎖;HashMap在多線程下最大的安全隐患發生在擴容的時候,想想一個場合:HashMap使用預設容量16,這時100個線程同時往HashMap中put元素,會發生什麼?擴容混亂,因為擴容也沒有任何鎖來保證并發安全,另外,後面的博文會講到ConcurrentHashMap的并發擴容操作是ConcurrentHashMap的一個核心方法。
HashMap的value允許為null,但是HashTable和ConcurrentHashMap的value 都不允許為null,試分析原因?
首先要明确ConcurrentHashMap和Hashtable從技術從技術層面講是可以允許value為null;但是它是實際是不允許的,這肯定是為了解決一些問題,為了說明這個問題,我們看下面這個例子(這裡以ConcurrentHashMap為例,HashTable也是類似)。
HashMap由于允value為null,get方法傳回null時有可能是map中沒有對應的key;也有可能是該key對應的value為null。是以get不能判斷map中是否包含某個key,隻能使用contains判斷是否包含某個key。
if (map.containsKey(k)) {
return map.get(k);
} else {
throw new KeyNotPresentException();
}
- 如果上面的map為HashMap,那麼沒什麼問題,因為HashMap本來就是線程不安全的,如果有并發問題應該用ConcurrentHashMap,是以在單線程下面可以傳回正确的結果
- 如果上面的map為ConcurrentHashMap,此時存在并發問題:在map.containsKey(k)和map.get之間有可能其他線程把這個key删除了,這時候map.get就會傳回null,而ConcurrentHashMap中不允許value為null,也就是這時候傳回了null,一個根本不允許出現的值?