1. 程式人生 > >HashMap&&HashTable底層原理以及常見面試題

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的區別。

  1. 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()方 法來判斷是否含有某個鍵。