java面試總結(三)------HashaMap、TreeMap
HashMap和TreeMap作爲最常用同時也是最容易被考察的點來說,掌握是至關重要的
-
HashMap:
基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。基於數組(Node[] table)和鏈表結合組成的複合結構,數組被分爲一個個桶(bucket),通過哈希值決 定了鍵值對在這個數組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,參考下面的示意圖。這裏 需要注意的是,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造爲樹形結構。
HashMap有四個構造函數,如下:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
從上至下分別是 無參(以默認負載因子構造HashMap)、帶初始容量參(以初始容量參和默認負載因子構造HashMap)、以初始容量和負載因子爲參、以另一個Map爲參(此處不做重點)。
我們着重看第三個構造函數,即以初始容量和負載因子爲參的構造函數,在源碼中,先經歷了一系列的非法性判斷,然後初始化負載因子,然後 tableSizeFor(initialCapacity)
,其中tableSizeFor(initialCapacity)
源碼如下:
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;
}
看不懂?沒關係 ,看這篇資料,這裏不做贅述,其實這個函數的意思是 返回大於輸入參數且最近的2的整數次冪的數
,即輸入 10
返回 16
,記住這個函數,這個會非常重要,那麼爲什麼要這麼做呢?請看文章最下面。
然後下面就講最基礎的幾個api
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int , i;
if ((tab = table) == null || (n = tab.length) = 0)
n = (tab = resize()).legth;
if ((p = tab[i = (n - 1) & hash]) == ull)
tab[i] = newNode(hash, key, value, nll);
else {
// ...
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for frs
treeifyBin(tab, hash);
// ...
}
}
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
在構造函數中,發現僅僅只是初始化了參數,並沒有進行其他操作,是按照lazy-load原則,在首次使用時被初始化(拷貝構造函數除外),然後看 put函數,裏面調用了putVal()
函數,
putVal()
解析如下:
-
如果表格是null,resize方法會負責初始化它,這從tab = resize()可以看出。
-
resize方法兼顧兩個職責,創建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)。
-
在放置新的鍵值對的過程中,如果發生下面條件,就會發生擴容。
if (++size > threshold)
resize();
- 具體鍵值對在哈希表中的位置(數組index)取決於下面的位運算
i = (n - 1) & hash
爲什麼要做以上的位運算呢,也請看文章最最下面的問題部分。
2. TreeMap
TreeMap稍後詳解
問題
-
擴容後,HashMap中原來的元素是怎麼儲存的
參考資料
即 :
如果無鏈,原來的元素,要麼在原位置,要麼在原位置+原數組長度 那個位置上。
如果有鏈,
查閱權威資料再更 -
爲什麼每次擴容後大小必須是2的n次方&&爲什麼求下標是(n - 1) & hash?
這兩個問題可以一起回答,在源碼中可以看到,每次擴容包括初始容量16必須是2的n次方,爲什麼呢?
其實很容易回答,先回答另一個問題,在默認情況下(容量=16)怎麼保證一個32位的二進制串在0-15中分佈?大部分同學可能回答是取餘,是的,在大部分情況看來,取餘似乎是個不錯的選擇,但是取餘會進行除法,比較慢,所以java8中提供了這麼一種方法:對於默認情況,16=2的4次方,轉成二進制即10000,然後按照源碼公式,
(10000-1)&hash
,如圖
這也就很好的解釋了爲什麼容量必須是2的n次方,是爲了滿足按位與得出下標值的運算的條件,其原理是容量-1的二進制一定全是1,然後再與hash值做 按位與 運算,就能得到一個處於 0 - 容量 的大小的二進制串,也就得到它的下標,所以直接使用位運算速度快,且分佈儘量在均勻範圍內。
如果容量不是2的n次方,那麼容量-1的二進制一定不全是1,如果用此值進行按位與操作,那麼某一位是0的情況下會導致某個哈系桶將永遠得不到儲存,就違背了儘量均勻分佈
的原則.
-
如果兩個鍵的hashcode相同,如何獲取值對象
找到參考資料再更 -
爲什麼默認負載因子是0.75
在理想情況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵循泊松分佈,同時給出了桶中元素個數和概率的對照表。從上面的表中可以看到當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作爲加載因子,每個碰撞位置的鏈表長度超過8個是幾乎不可能的.
參考
參考資料 :
HashMap