1.源碼 java
java1.7 hashMap 底層實現是數組+鏈表node
java1.8 對上面進行優化 數組+鏈表+紅黑樹面試
2.hashmap 是怎麼保存數據的。segmentfault
在hashmap 中有這樣一個結構數組
Node implenets Map.entity{安全
hash數據結構
key多線程
valueide
next函數
}
當咱們像hashMap 中放入數據時,其實就是一個
Enity{
key
vaue
}
在存以前會把這個Entity 轉成Node
怎麼轉的以下:
根據Entity 的key 經過hash 算出一個值 當成Node 的 hash ,key vlaue ,複製到Node 中,對於沒有產生hash 衝突前,Node 的next 是null.
複製完成後,還須要經過Entity 對象的hash 算出 該 Entiry對象 具體應該放在 hashMap 的那個位置。計算以下 Hash&(lenth-1) 獲得的值就是hashMap 對應具體的位置。(lentth是當前hashMap 的長度)。‘
解決hash 衝突
就是不一樣的元素經過 Hash&(lenth-1) 公式算出的位置相同,如今就啓動了鏈表(單項鍊表),掛在了當前位置的下面,而鏈表的元素怎麼關聯呢,就用到了Node 的next ,next的值就是該鏈表下一個元素在內存中的地址。
jdk1.7 就是這樣處理的,而到了 1.8 之後,就引用了紅黑樹,1.8之後這個鏈表只讓掛7個元素,超過七個就會轉成一個紅黑樹進行處理(最可能是64,超多64 就會從新拆分)。
當紅黑樹下掛的節點小於等於6的時候,系統會把紅黑樹轉成鏈表。 Node 在jdk1.8以前是插入l鏈表頭部的,在jdk1.8中是插入鏈表的尾部的。
hashMap 擴容:
hashMap 會根據 當前的hashMap 的存儲量達到 16*0.75=12 的時候,就是擴容 16*2=32 依次類推下去。2倍擴容。
擴容後元素是如何作到均勻分的。
對上面的總結:
這位大哥寫的很好,能夠看一下 https://segmentfault.com/a/1190000012964859
我針對LinkedHashMap 的總結有一下幾點
1.LinkedHashMap 繼承自 HashMap,因此它的底層仍然是基於拉鍊式散列結構。該結構由數組和鏈表+紅黑樹 在此基礎上LinkedHashMap 增長了一條雙向鏈表,保持遍歷順序和插入順序一致的問題。
2. 在實現上,LinkedHashMap 不少方法直接繼承自 HashMap(好比put remove方法就是直接用的父類的),僅爲維護雙向鏈表覆寫了部分方法(get()方法是重寫的)。
3.LinkedHashMap使用的鍵值對節點是Entity 他繼承了hashMap 的Node,並新增了兩個引用,分別是 before 和 after。這兩個引用的用途不難理解,也就是用於維護雙向鏈表.
4.鏈表的創建過程是在插入鍵值對節點時開始的,初始狀況下,讓 LinkedHashMap 的 head 和 tail 引用同時指向新節點,鏈表就算創建起來了。隨後不斷有新節點插入,經過將新節點接在 tail 引用指向節點的後面,便可實現鏈表的更新
5.LinkedHashMap 容許使用null值和null鍵, 線程是不安全的,雖然底層使用了雙線鏈表,可是增刪相快了。由於他底層的Entity 保留了hashMap node 的next 屬性。
6.如何實現迭代有序?
從新定義了數組中保存的元素Entry(繼承於HashMap.node),該Entry除了保存當前對象的引用外,還保存了其上一個元素before和下一個元素after的引用,從而在哈希表的基礎上又構成了雙向連接列表。仍然保留next屬性,因此既可像HashMap同樣快速查找,
用next獲取該鏈表下一個Entry,也能夠經過雙向連接,經過after完成全部數據的有序迭代.
7.居然inkHashMap 的put 方法是直接調用父類hashMap的,但在 HashMap 中,put 方法插入的是 HashMap 內部類 Node 類型的節點,該類型的節點並不具有與 LinkedHashMap 內部類 Entry 及其子類型節點組成鏈表的能力。那麼,LinkedHashMap 是怎樣創建鏈表的呢?
雖然linkHashMap 調用的是hashMap中的put 方法,可是linkHashMap 重寫了,了一部分方法,其中就有 newNode(int hash, K key, V value, Node<K,V> e)
linkNodeLast(LinkedHashMap.Entry<K,V> p)
這兩個方法就是 第一個方法就是新建一個 linkHasnMap 的Entity 方法,而
linkNodeLast 方法就是爲了把Entity 接在鏈表的尾部。
8.鏈表節點的刪除過程
與插入操做同樣,LinkedHashMap 刪除操做相關的代碼也是直接用父類的實現,可是LinkHashMap 重寫了removeNode()方法
afterNodeRemoval
()方法,該removeNode方法在hashMap 刪除的基礎上有調用了afterNodeRemoval
回調方法。完成刪除。
刪除的過程並不複雜,上面這麼多代碼其實就作了三件事:
TreeMap 和SortMap
1.TreeMap實現了SortedMap接口,保證了有序性。默認的排序是根據key值進行升序排序,也能夠重寫comparator方法來根據value進行排序具體取決於使用的構造方法。不容許有null值null鍵。TreeMap是線程不安全的。
2.TreeMap基於紅黑樹(Red-Black tree)實現。TreeMap的基本操做 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
public class SortedMapTest {
public static void main(String[] args) {
SortedMap<String,String> sortedMap = new TreeMap<String,String>();
sortedMap.put("1", "a");
sortedMap.put("5", "b");
sortedMap.put("2", "c");
sortedMap.put("4", "d");
sortedMap.put("3", "e");
Set<Entry<String, String>> entry2 = sortedMap.entrySet();
for(Entry<String, String> temp : entry2){
System.out.println("修改前 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
System.out.println("\n");
//這裏將map.entrySet()轉換成list
List<Map.Entry<String,String>> list =
new ArrayList<Map.Entry<String,String>>(entry2);
Collections.sort(list, new Comparator<Map.Entry<String,String>>(){
@Override
public int compare(Entry<String, String> o1, Entry<String, String> o2) {
// TODO Auto-generated method stub
return o1.getValue().compareTo(o2.getValue());
}
});
for(Map.Entry<String,String> temp :list){
System.out.println("修改後 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
}
}
附加點上面沒有講到的面試題:
1 HashMap特性?
HashMap的特性:HashMap存儲鍵值對,實現快速存取數據;容許null鍵/值;線程不安全;不保證有序(好比插入的順序)。
2 HashMap中hash函數怎麼是是實現的?還有哪些 hash 的實現方式?
1. 對key的hashCode作hash操做(高16bit不變,低16bit和高16bit作了一個異或);
2. h & (length-1); //經過位操做獲得下標index。
3. 擴展問題1:當兩個對象的hashcode相同會發生什麼?
由於兩個對象的Hashcode相同,因此它們的bucket位置相同,會發生「碰撞」。HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。
4 擴展問題2:拋開 HashMap,hash 衝突有那些解決辦法?
開放定址法、鏈地址法、再哈希法。
5若是兩個鍵的hashcode相同,你如何獲取值對象?
重點在於理解hashCode()與equals()。
經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。兩個鍵的hashcode相同會產生碰撞,則利用key.equals()方法去鏈表或樹(java1.8)中去查找對應的節點。
6 針對 HashMap 中某個 Entry 鏈太長,查找的時間複雜度可能達到 O(n),怎麼優化?
將鏈表轉爲紅黑樹,實現 O(logn) 時間複雜度內查找。JDK1.8 已經實現了。
7.若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
擴容。這個過程也叫做rehashing,由於它重建內部數據結構,並調用hash方法找到新的bucket位置。
大體分兩步:
1.擴容:容量擴充爲原來的兩倍(2 * table.length);
2.移動:對每一個節點從新計算哈希值,從新計算每一個元素在數組中的位置,將原來的元素移動到新的哈希表中。 (如何計算上面講的有)
8 爲何String, Interger這樣的類適合做爲鍵?
String, Interger這樣的類做爲HashMap的鍵是再適合不過了,並且String最爲經常使用。
由於String對象是不可變的,並且已經重寫了equals()和hashCode()方法了。
1.不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其餘的優勢如線程安全。
注:String的不可變性能夠看這篇文章《【java基礎】淺析String》。
2.由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的。若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這樣就能提升HashMap的性能。
9.hashmap.put 爲何是線程不安全的。(很重要)
正常狀況下 hashmap 在保存數據時,底層是數組+鏈表+紅黑樹 可是 你去源碼中看時,i發現子啊hashMap 底層沒有加任何的多線程中的鎖機制,好比: synchronize修飾 ,因此在多線程的狀況下 hashMap 的單項鍊表,
可能會變成一個環形的鏈表,因此這個鏈表上的Next元素的指向永遠不爲null, 因此在遍歷的時候就是死循環啊。
9.1HashMap在put的時候,插入的元素超過了容量(由負載因子決定)的範圍就會觸發擴容操做,就是rehash,這個會從新將原數組的內容從新hash到新的擴容數組中,在多線程的環境下,存在同時其餘的元素也在進行put操做,若是hash值相同,可能出現同時在同一數組下用鏈表表示,形成閉環,致使在get時會出現死循環,因此HashMap是線程不安全的
9.2 HashMap底層是一個Entry數組,當發生hash衝突的時候,hashmap是採用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。在hashmap作put操做的時候會調用到以上的方法。如今假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時獲得如今的頭結點,而後A寫入新的頭結點以後,B也寫入新的頭結點,那B的寫入操做就會覆蓋A的寫入操做形成A的寫入操做丟失
10 ,hashmap 初始化時就生了一個長度爲16 的數組。
1.1 爲何初始化時16 而不是別的數字,
1.實際上是4或者8 只要是2的N次冪就行,由於hashMap put 元素時,會根據key 進行運算獲得一個位置,運算就是,根據key的hash值&hashMap的長度-1(hash&length-1) ,
假如hashMap的長度爲16 補充:&運算時,都是1才爲1,其餘狀況都爲0
hash值 1010 1010 1000 0000 .... 1010
&
lennth-1 0111
你會發現無論hash值爲多少,只要 length 的長度是2的N次冪, 那麼length-1 的二進制最後一位就是1,因此 hash值&上length-1 最後獲得的二進制數字的末位,多是1 也多是0,
若是 其長度不是2的n次冪,好比 15 ,那麼15-1 =14 的 二進制 0110,那麼趕上hash 的到二進制末位,永遠就是0了 ,這就側面的代表了經過計算出來的元素位置的分散性。
爲何不選4,8 這些也是2的N次冪做爲擴容初始化值呢?其實8 也行4 也行,可是 個人java 是c語言寫的,而c語言是由彙編語言,而彙編的語言來源是機器語言,而彙編的語言使用的就是16進制,對於經驗而言,固然就選16嘍。