去面試時,hashmap老是被常常問的問題,下面總結了幾道關於hashmap的問題。面試
一、hashmap的主要參數都有哪些?算法
二、hashmap的數據結構是什麼樣子的?本身如何實現一個hashmap?數組
三、hash計算規則是什麼?數據結構
四、說說hashmap的存取過程?函數
五、說說hashmap如何處理碰撞的,或者說說它的擴容?this
解答:以1.7爲例,也會摻雜一些1.8的不一樣點。spa
一、code
1)桶(capacity)容量,即數組長度:DEFAULT_INITIAL_CAPACITY=1<<4;默認值爲16對象
即在不提供有參構造的時候,聲明的hashmap的桶容量;blog
2)MAXIMUM_CAPACITY = 1 << 30;
極限容量,表示hashmap能承受的最大桶容量爲2的30次方,超過這個容量將再也不擴容,讓hash碰撞起來吧!
3)static final float DEFAULT_LOAD_FACTOR = 0.75f;
負載因子(loadfactor,默認0.75),負載因子有個奇特的效果,表示噹噹前容量大於(size/)時,將進行hashmap的擴容,擴容通常爲擴容爲原來的兩倍。
4)int threshold;閾值
閾值算法爲capacity*loadfactory,大體當map中entry數量大於此閾值時進行擴容(1.8)
5)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;(默認爲空{})
核心的數據結構,即所謂的數組+鏈表的部分。
二、hashmap的數據結構是什麼樣子的?本身如何實現一個hashmap?
主要數據結構即爲數組+鏈表。
在hashmap中的主要表現形式爲一個table,類型爲Entry<K,V>[] table
首先是一個Entry型的數組,Entry爲hashmap的內部類:
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 int hash; 6 }
在這裏能夠看到,在Entry類中存在next,因此,它又是鏈表的形式。
這就是hashmap的主要數據結構。
三、hash的計算規則,這又要看源碼了:
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
這是1.8的源碼,1.7太複雜但原理是一致的,簡單說這就是個「擾動函數」,最終的目的是讓散列分佈地更加均勻。
算法就是拿存儲key的hashcode值先右移16位,再與hashcode值進行亦或操做,即不求進位只求按位相加的值:盜圖:
最後是如何得到,本key在table中的位置呢?自己應該是取得了hash進行磨除取餘運算,可是,源碼:
1 static int indexFor(int h, int length) { 2 // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; 3 return h & (length-1); 4 }
爲何又作了個與運算求得位置呢?簡單說,它的意義和取餘一致。
不信能夠本身算一下。
首先說,他利用了table的長度確定是2的整數次冪的原理,假設當前length爲16,2的4次方
而與&運算,又是隻求進位運算,好比1111&110001結果爲000001
只求進位運算(&),保證算出的結果必定在table的length以內,最大爲1111。
故而,它的運算結果與價值等同於取餘運算,而且即便無論hash值有多大均可以算出結果,而且在length以內。
而且,這種類型的運算,可以更加的節約計算機資源,少了加(計算機全部運算都是加運行)運算過程,更加地節省資源。
四、hashmap的存取過程
源碼1.7:
1 /** 2 *往hashmap中放數據 3 */ 4 public V put(K key, V value) { 5 if (table == EMPTY_TABLE) { 6 inflateTable(threshold);//判斷若是爲空table,先對table進行構造 7 //構造經過前面的幾個參數 8 } 9 //首先判斷key是否爲null,爲null也能夠存 10 //這裏須要記住,null的key必定放在table的0號位置 11 if (key == null) 12 return putForNullKey(value); 13 //算出key的hash值 14 int hash = hash(key); 15 //根據hash值算出在table中的位置 16 int i = indexFor(hash, table.length); 17 //放入K\V,遍歷鏈表,若是位置上存在相同key,進行替換value爲新的,且將替換的舊的value返回 18 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 19 Object k; 20 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 21 V oldValue = e.value; 22 e.value = value; 23 e.recordAccess(this); 24 return oldValue; 25 } 26 } 27 modCount++; 28 //增長一個entry,有兩種狀況,一、若是此位置存在entry,將此位置變爲插入的entry,且將插入entry的next節點變爲原來的entry;二、若是此位置不存在entry則直接插入新的entry 29 addEntry(hash, key, value, i); 30 return null; 31 }
取數據:
1 //根據key得到一個entry 2 public V get(Object key) { 3 //若是key爲null,獲取0號位的切key爲null的值 4 if (key == null) 5 return getForNullKey(); 6 //若是不是,獲取entry,在下面方法 7 Entry<K,V> entry = getEntry(key); 8 //合法性判斷 9 return null == entry ? null : entry.getValue(); 10 } 11 //獲取一個key不爲null的entry 12 final Entry<K,V> getEntry(Object key) { 13 //若是table爲null,則返回null 14 if (size == 0) { 15 return null; 16 } 17 //計算hash值 18 int hash = (key == null) ? 0 : hash(key); 19 //根據hash值得到table的下標,遍歷鏈表,尋找key,找到則返回 20 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 21 e != null; 22 e = e.next) { 23 Object k; 24 if (e.hash == hash && 25 ((k = e.key) == key || (key != null && key.equals(k)))) 26 return e; 27 } 28 return null; 29 }
5.擴容和碰撞
先說碰撞吧,因爲hashmap在存值的時候並非直接使用的key的hashcode,而是經過擾動函數算出了一個新的hash值,這個計算出的hash值能夠明顯的減小碰撞。
還有一種解決碰撞的方式就是擴容,擴容其實很好理解,就是將原來桶的容量擴爲原來的兩倍。這樣爭取散列的均勻,好比:
原來桶的長度爲16,hash值爲1和17的entry將會都在桶的0號位上,這樣就出現了碰撞,而當桶擴容爲原來的2倍時,hash值爲1和17的entry分別在1和17號位上,整號岔開了碰撞。
下面說說什麼時候擴容,擴容都作了什麼。
1.7中,在put元素的過程當中,判斷table不爲空、切新增的元素的key不與原來的重合以後,進行新增一個entry的邏輯。
1 void addEntry(int hash, K key, V value, int bucketIndex) { 2 if ((size >= threshold) && (null != table[bucketIndex])) { 3 resize(2 * table.length); 4 hash = (null != key) ? hash(key) : 0; 5 bucketIndex = indexFor(hash, table.length); 6 } 7 createEntry(hash, key, value, bucketIndex); 8 }
由源代碼可知,在新增元素時,會先判斷:
1)當前的entry數量是否大於或者等於閾值(loadfactory*capacity);
2)判斷當前table的位置是否存在entry。
經上兩個條件聯合斷定,纔會進行數組的擴容工做,最後擴容完成纔會去建立新的entry。
而擴容的方法即爲:resize()看代碼
1 void resize(int newCapacity) { 2 //拿到原table對象 3 Entry[] oldTable = table; 4 //計算原table的桶長度 5 int oldCapacity = oldTable.length; 6 //先斷定,當前容量是否已是最大容量了(2的30次方) 7 if (oldCapacity == MAXIMUM_CAPACITY) { 8 //假如達到了,將閾值設爲int的最大值2的31次方減1,返回 9 threshold = Integer.MAX_VALUE; 10 return; 11 } 12 //建立新的table對象 13 Entry[] newTable = new Entry[newCapacity]; 14 //將舊的table放入新的table中 15 transfer(newTable, initHashSeedAsNeeded(newCapacity)); 16 //賦值新table 17 table = newTable; 18 //計算新的閾值 19 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 20 } 21 //具體的擴容過程 22 void transfer(Entry[] newTable, boolean rehash) { 23 int newCapacity = newTable.length; 24 //遍歷原table,從新散列 25 for (Entry<K,V> e : table) { 26 while(null != e) { 27 Entry<K,V> next = e.next; 28 if (rehash) { 29 e.hash = null == e.key ? 0 : hash(e.key); 30 } 31 int i = indexFor(e.hash, newCapacity); 32 e.next = newTable[i]; 33 newTable[i] = e; 34 e = next; 35 } 36 } 37 }
至此,擴容就說完了。。。