HashMap&&HashTable底層原理以及常見面試題
1.HashMap VS HashTable
1.1.首先說下 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; ... }
HashMap儲存函式的實現put(K key, V value) 根據下面put方法的原始碼可以看出,當程式試圖將一個key-value對放入HashMap中時,**1.**程式首先計算該key的hashCode()值,**2.**然後對該雜湊碼值進行再雜湊,**3.**然後把雜湊值和(陣列長度-1)進行按位與操作,得到儲存的陣列下標,**4.**如果該位置處設有連結串列節點,那麼就直接把包含<key,value>的節點放入該位置。如果該位置有結點,就對連結串列進行遍歷,看是否有hash,key和要放入的節點相同的節點,如果有的話,就替換該節點的value值,如果沒有相同的話,就建立節點放入值,並把該節點插入到連結串列表頭(頭插法)
public V put(K key, V value) { //HashMap允許存放null鍵和null值。 //當key為nu11時, 呼叫putForNullKey方法,將valve放置在陣列第一個位置。 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 8& ((k = e.key) == key II key. equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //如果讀引處的Entry為null,表明此處還沒有Entry omodCount++; //將key、 value新增到索引處。 addEntry(hash, key, value, i); return null; }
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);
//如果Hap中的key-value對的數裡超過了極限
if (size++ >= threshold)
//把table物件的長度擴充到原理的2倍
resize(2*table.length);
}
二倍擴容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
static int hash(int h){
h ^ =(h>>>20)^ (h>>>12);
return h ^ (h>>>7) ^ (h>>>4);
}
擴充套件:為何陣列的長度是2的n次方呢? 1.這個方法非常巧妙,它通過h & (table.length-1)來得到該物件的儲存位,而HashMap底層陣列的長度總是2的n次方,2^n -1得到的二進位制數的每個位上的值都為1,那麼與全部為1的一一個數進行與操作,速度會大大提升。 2.當length總是2的n次方時,h& (length-1)運 算等價王對length取模,也就是h%length,但是&比%縣有更高的效率。 3.當陣列長度為2的n次冪的時候,不同的key算得的index相同的機率較小,那麼資料在陣列上分佈就比較均勻,也就是說碰擁的機率小,相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢效率也就較高了。 HashMap讀取函式的實現get
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) {
0bject k;
if (e.hash - hash 8& ((K”e.key) - keyII key.equals(k))
return e.value;
}
return null;
}
hashMap的get方法1. 是首先通過key的兩次hash後的值與陣列的長度-1進行與操作,定位到陣列的某個位置,2. 然後對該列的連結串列進行遍歷,一般情況下,hashMap的這種查詢速度是非常快的,hash 值相同的元(O就會造成連結串列中資料很多,而連結串列中的資料查詢是通過應歷所有連結串列中的元素進行的,這可能會影響到查詢速度,找到即返回。特別注意:當返回為null時,你不能判斷是沒有找到指定元素,還是在hashmap中存著一一個value為null的元素,因為hashmap允許value為null. HashMap的擴容機制: 當HashMap中的結點個數超過陣列大小loadEactor*(載入因子)時,就會進行陣列擴容,loadFactor的預設值為0.75.世就是說,預設情況下,陣列大小為16,那麼當HashMap電結點個數超過160.75=12的時候,就把陣列的大小和展為216=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,並放進去,而這是一個非常消耗效能的操作。 多執行緒下HashMap出現的問題: 1.多執行緒put操作後,get操作導致死迴圈導致cpu100%的現象。主要是多執行緒同時put時,如果同時觸發了rehash操作,會導政擴客後的HashMap中的連結串列中出現迴圈節點進而使得後面get的時候,會死迴圈。 2.多執行緒put操作,導致元素丟失,也是發生在個執行緒對hashmap 擴容時。
2.hashTable的原理。
它的原理和hashMap基本一致。
3.HashMap和HashTable的區別。
- Hashtable 是執行緒安全的,方法是Synchronized 的,適合在多執行緒環境中使用,效率稍低: HashMap不是執行緒安全的,方法不是Synchronized的,效率稍高,適合在單執行緒環境下使用,所以在多執行緒場合下使用的話,需要手動同步HashMap,Collctions. synchronizedMap()。 PS:HashTable的效率比較低的原因? 線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪間HashTable的同步方法時,訪問其他同步方法的執行緒就可能會進入阻嘉或者輪訓狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈改率越低. 2.HashMap的key和value都可以為null值,HashTable 的key和value都不允許L Null值。 3.HashMap中陣列的預設大小是16,而且一定是2的倍數,擴容後的陣列長度是之前陣列長度的2倍。HashTable中陣列預設大小是11.擴容後的陣列長度是之前陣列長度的2倍+1。 4.雜湊直的使用不同。 而HashMap重新計算hash值,面且用&代替求模:
int hash = hash(key.hashcode0);
int i= indexFor(hash, table.length);
static int hash(Objectx) {
int h = x.hashCode();
h += ~(h<<9);h^= (h>>> 14);
h+=(h<< 4);
h ^= (h>>> 10);
returm h;
}
static int indexFor(int h, int length) {
return h & (length-1); //hashmap的表長永遠是2^n。
}
HashTable直接使用物件的hashCode值: int hash = key.hashCode(); //注意區分2者的hash值!! int index = (hash & 0x7FFFFFFF) % tab.length;
4.判斷是否含有某個鍵
在HashMap中,null 可以作為鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值為null,當get()方法返回null值時,既可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能用get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判析。Hashtable的鍵值都不能為null,所以可以用get()方 法來判斷是否含有某個鍵。