hashmap和hashtable實現原理
一、HashMap
1.HashMap概述:
HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。
2.HashMap的資料結構:
在Java程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來構造,HashMap也不例外。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合體。如下圖,HashMap底層就是一個數組結構,陣列中的每一項又是一個連結串列。當新建一個HashMap的時候,就會初始化一個數組。
原始碼:
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
Entry就是陣列中的元素,每個Map.Entry其實就是一個Key-Value對,它持有一個指向下一個元素的引用,這就構成了連結串列。
3.HashMap的儲存實現:
public V put(K key, V value) {
// HashMap允許存放null鍵和null值。
// 當key為null時,呼叫putForNullKey方法,將value放置在陣列第一個位置。
if (key == null)
return putForNullKey(value);
// 根據key的keyCode重新計算hash值。
int hash = hash(key.hashCode());
// 搜尋指定hash值在對應table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引處的 Entry 不為 null,通過迴圈不斷遍歷 e 元素的下一個元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引處的Entry為null,表明此處還沒有Entry。
modCount++;
// 將key、value新增到i索引處。
addEntry(hash, key, value, i);
return null;
}
從原始碼得:當我們往HashMap中put元素時,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在陣列的位置(下標),如果陣列該位置已經存放了其他元素了,那麼在這個位置上的元素將以連結串列的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。
addEntry(hash,key,value,i)方法根據計算出的hash值,將Key-Value對放在陣列table的i索引處。addEntry是HashMap提供的一個包訪問許可權的方法,方法如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
// 獲取指定 bucketIndex 索引處的 Entry
Entry<K,V> e = table[bucketIndex];
// 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 對的數量超過了極限
if (size++ >= threshold)
// 把 table 物件的長度擴充到原來的2倍。
resize(2 * table.length);
}
當系統決定儲存HashMap中的Key-Value對時,完全沒有考慮Entry中的Value,僅僅只是根據key來計算並決定每個Entry的儲存位置。即當系統決定了key的儲存位置之後,value隨之儲存在那裡即可。
hash(int h)方法根據key的hashCode重新計算一次雜湊。此演算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
我們得知在HashMap中要找到某個元素,需要根據key得到hash值來求得對應陣列中的位置。如何計算這個位置就是hash演算法。上面說過HashMap的資料結構是陣列和連結串列的結合,所以我們當然希望這個HashMap中的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷連結串列,這樣就大大的優化了查詢的效率。
對於任意給定的物件,只要它的HashCode()返回值相同,那麼程式呼叫Hash(int h)方法所計算得到的hash值總是相同的。我們首先想到的就是把Hash值對陣列的長度取模運算,這麼一來,元素的分佈相對來說是比較均勻的。但是模運算的消耗還是比較大的,HashMap中:呼叫indexFor(int h,int length)方法來計算該物件應該儲存在table陣列的哪個索引處。
static int indexFor(int h, int length) {
return h & (length-1);
}
HashMap底層陣列的長度總是2的n次方,這是HashMap在速度上的優化。
當陣列長度為2的n次冪的時候,不同的Key算得的index相同的機率較小,那麼資料在陣列上分佈就比較均勻,也就是碰撞的機率·比較小。相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢的效率也就比較高了。
4.HashMap的讀取實現:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
從HashMap中get元素時,首先計算key的hashCode,找到陣列中對應位置的某一個元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。即:HashMap在底層將key-Value當成一個整體進行處理,這個整體就是一個Entry物件。HashMap底層採用了一個Entry[]陣列來儲存所有的鍵值對,當需要儲存一個Entry物件時,會根據hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry時,也會根據hash演算法找到其在陣列中的儲存位置,在根據equals方法從該位置上的連結串列中取出該Entry。
5.HashMap的resize(rehash):
當hashMap中的元素越來越多的時候,hash衝突的機率也越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對HashMap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize。當HashMap中的元素個數超過陣列大小loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把陣列的大小擴容為2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。
6.Fail-Fast機制:
我們知道HashMap不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了Map,那麼將丟擲ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在原始碼中的實現是用過modCount域(修改次數)對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectModCount。在迭代過程中,判斷modCount跟expectModCount是否相等,如果不想等就表示已經有其他執行緒修改了Map。(modCount宣告為volatile,保證了執行緒之間的可見性)。
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
7.關於HashMap的問題:
為什麼String、Integer這樣的wrapper類適合作為鍵?
String、Integer這樣的wrapper類作為HashMap的鍵是在適合不過了,而且String最為常用,因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算HashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashCode的話,那麼就不能從HashMap中找到你想要的物件。不可變性還有其他的優點如執行緒安全。如果你可以僅僅通過將某個field宣告成final就能保證hashCode是不變的,那麼請這麼做。因為獲取物件的時候要用到equals()和hashCode()方法,那麼鍵物件正確的重寫這兩個方法是非常重要的。如果兩個不相等的物件返回不同的hashCode的話,那麼碰撞的機率就會小些,這樣就能提高HashMap的效能。
我們可以使用自定義的物件作為鍵嗎?
這是前一個問題的延伸。當然你可能使用任何物件作為鍵,只要它遵循了equals()和hashCode()方法的定義規則,並且當物件插入到Map中之後將不會再改變了。如果這個自定義物件時不可變的,那麼它已經滿足了作為鍵的條件,因為當它建立之後就已經不能改變了。
我們可以使用ConcurrentHashMap來代替HashTable嗎?
這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道HashTable是synchronized的,但是ConcurrentHashMap同步效能更好,因為它僅僅根據同步級別對Map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的執行緒安全性。
二、HashTable
1.HashTable概述:
和HashMap一樣,HashTable也是一個散列表,它儲存的內容是鍵值對對映。HashTable繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable介面。HashTable的函式都是同步的,這意味著它是執行緒安全的。它的Key、Value都不可以為null。此外,HashTable中的對映不是有序的。
HashTable的例項有兩個引數影響其效能:初始容量和載入因子。容量是雜湊表中桶的數量,初始容量就是雜湊表建立時的容量。注意,雜湊表的狀態為open:在發生“雜湊衝突”的情況下,單個桶會儲存多個條目,這些條目必須按順序搜尋。載入因子是對雜湊表在其容量自動增加之前可以達到多滿的一個尺度。初始容量和載入因子這兩個引數只是對該實現的提示。關於何時以及是否呼叫rehash方法的具體細節則依賴於該實現。通常,預設載入因子是0.75。
2.Hash Table的資料結構:
HashTable與Map關係如下
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
HashTable並沒有去繼承AbstractMap,而是選擇繼承了Dictionary類,Dictionary是個被廢棄的抽象類。
3.實現原理:
成員變數跟HashMap基本類似,但是HashMap更加規範,HashMap內部還定義了一些常量,比如預設的負載因子,預設的容量,最大容量等。
public Hashtable(int initialCapacity, float loadFactor) {//可指定初始容量和載入因子
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;//初始容量最小值為1
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];//建立桶陣列
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化容量閾值
useAltHashing = sun.misc.VM.isBooted() &&
(initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
}
/**
* Constructs a new, empty hashtable with the specified initial capacity
* and default load factor (0.75).
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);//預設負載因子為0.75
}
public Hashtable() {
this(11, 0.75f);//預設容量為11,負載因子為0.75
}
/**
* Constructs a new hashtable with the same mappings as the given
* Map. The hashtable is created with an initial capacity sufficient to
* hold the mappings in the given Map and a default load factor (0.75).
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
——HashTable的預設容量為11,預設負載因子為0.75。(HashMap預設容量是16,預設負載因子也是0.75)
——HashTable的容量可以為任意整數,最小值為1,而HashMap的容量始終為2的n次方。
——為避免擴容帶來的效能問題,建議指定合理容量。跟HashMap一樣,HashTable內部也有一個靜態類叫Entry,其實是個鍵值對,儲存了鍵和值的引用。也可以理解為一個單鏈表的節點,因為其持有下一個Entry物件的引用。
4.HashTable的存取實現:
HashTable和HashMap儲存的都是鍵值對物件,而不是單獨的鍵或值。
public synchronized V put(K key, V value) {//向雜湊表中新增鍵值對
// Make sure the value is not null
if (value == null) {//確保值不能為空
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = hash(key);//根據鍵生成hash值---->若key為null,此方法會拋異常
int index = (hash & 0x7FFFFFFF) % tab.length;//通過hash值找到其儲存位置
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {/遍歷連結串列
if ((e.hash == hash) && e.key.equals(key)) {//若鍵相同,則新值覆蓋舊值
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//當前容量超過閾值。需要擴容
// Rehash the table if the threshold is exceeded
rehash();//重新構建桶陣列,並對陣列中所有鍵值對重雜湊,耗時!
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;//這裡是取摸運算
}
// Creates the new entry.
Entry<K,V> e = tab[index];
//將新結點插到連結串列首部
tab[index] = new Entry<>(hash, key, value, e);//生成一個新結點
count++;
return null;
}
public synchronized V get(Object key) {//根據鍵取出對應索引
Entry tab[] = table;
int hash = hash(key);//先根據key計算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//再根據hash值找到索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍歷entry鏈
if ((e.hash == hash) && e.key.equals(key)) {//若找到該鍵
return e.value;//返回對應的值
}
}
return null;//否則返回null
}
——HashTable並不允許值和鍵為空,若為空,則丟擲空指標異常。
——HashMap計算索引的方式是h&(length-1),而HashTable用的是模運算,效率上是低於HashMap的。
——HashTable計算索引時將hash值先與上0x7fffffff,這是為了保證hash值始終為整數。
——HashTable中若干方法都添加了synchronized關鍵字,也就意味著這個HashTable是個執行緒安全的類,這是它與HashMap最大的不同點。
——HashTable每次擴容都是舊容量的2倍加2,而HashMap為舊容量的2倍。