1.源碼
java1.7 hashMap 底層實現是數組+鏈表
java1.8 對上面進行優化 數組+鏈表+紅黑樹
2.hashmap 是怎么保存數據的。
在hashmap 中有這樣一個結構
Node implenets Map.entity{
hash
key
value
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倍擴容。
擴容后元素是如何做到均勻分的。
對上面的總結:
LinkedHashMap 源碼詳細分析(JDK1.8)
這位大哥寫的很好,可以看一下 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
回調方法。完成刪除。
刪除的過程並不復雜,上面這么多代碼其實就做了三件事:
- 根據 hash 定位到桶位置
- 遍歷鏈表或調用紅黑樹相關的刪除方法
- 從 LinkedHashMap 維護的雙鏈表中移除要刪除的節點
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嘍。