Map是由一對對的key-value組成的,key要求是唯一的,value不要求。
通過看源碼可以得出:key自帶去重功能是Set類型的,value是Collection接口可存放任意集合。
來看一下Map的實現類:
HashMap、HashTable、ConcurrentHashMap之間的區別?
HashMap
JDK8之前HashMap是由數組+鏈表組成的,數組查找快增刪慢,鏈表增刪快查找慢,HashMap結合了兩者的優勢,HashMap不是線程安全的效率很高,組織鍵位如圖:
沒有給HashMap初始長度的時候HashMap默認初始長度是16,初始長度16的數組中每個數組的位置存放的是鏈表的頭結點,可以通過模運算得到頭結點的存放位置:hash(key.hashCode())%len,hashCode的運算本身是通過位運算得到的。
但是存在一種特殊情況,通過模運算得到的位置每次都是同一個這樣的話就不斷在一條鏈表中去插入,最壞的情況是時間複雜度從O(1)變成O(n)。
JDK8之後對HashMap進行了優化,將原先的HashMap由數組+鏈表組成的道理變成了數組+鏈表+紅黑樹。
添加紅黑樹之後再次遇到特殊的情況時就可以使用TREEIFY_THRESHOLD去判斷是否將鏈表轉換成一顆紅黑樹,這種情況下最壞的時間複雜度從O(n)變成O(nlogn)。
下面來看一下源碼:
首先是HashMap的結點結構:
可以看出是一個數組,數組中的元素是一個個的頭結點,看一下Node的內部構造:
hash是用來尋址的,next是用來連接下一個鏈表或者樹的結點的。
接下來看一下從鏈表變成紅黑樹的邊界:
當長度超過8鏈表會從鏈表變成紅黑樹,並且有TREEIFY_THRESHOLD,使鏈表變成紅黑樹來提高性能,如果刪除結點,讓出結點也是必然存在策略使紅黑樹變成鏈表的邊界的。
就是UNTREEIFY_THRESHOLD等於6的時候,從紅黑樹變成鏈表。
看過HashMap的成員變量之後,再來看一下HashMap的構造函數:
通過loadFactor可以判斷出來HashMap是懶加載的,就是說當創建的時候不會去分配加載空間,只有在使用的時候纔會去初始化和Hibernate的延遲加載是一樣的。
當put時候纔是初始化的時候,下面來看看一下put的方法:
put實際上使用的是putVal方法,看看其方法的實現邏輯:
當第一次使用時候總和超過16的時候,HashMap會去分配合適的空間去使用,並且在裏面存在添加之後是否樹化,分配尋址空間等等細節的邏輯。
對HashMap的put方法簡單總結一下:
- 如果HashMap沒有被初始化,則初始化;
- 對key求hash值,然後再計算下標;
- 如果沒有碰撞,直接放入桶中;
- 如果碰撞以鏈表的方式鏈接到後面;
- 如果鏈表長度超過閾值,把鏈表轉換成紅黑樹;
- 如果鏈表長度低於6,將紅黑樹轉換成鏈表;
- 如果節點已經存在,就替換舊值;
- 如果桶滿了(容量16 * 0.6),需要resize擴容二倍之後重排。
再來看一下HashMap的get方法的實現邏輯:
主要是實現邏輯是使用hash算法找到對應的bucket桶,然後用key.equals找到地址空間,最後找到值返回。
除了樹化這種方式被動的提升性能之外hash運算也是可以提升性能的,但是hash運算是存在哈希碰撞的下面是減少hash碰撞概率的方法:
- 擾動函數:促使元素位置分佈均勻,減少碰撞機率;
- 使用final對象:採用合適的equals()和hashCode()方法。
HashMap擴容的問題:
- 多線程情況下,調整大小存在條件競爭,容易發生死鎖;
- rehashing是一個比較耗時的過程。