一、List和Set以及Map
1、List , Set, Map都是接口,前兩個繼承至Collection接口(Collection接口下還有個Queue接口,有PriorityQueue類),Map為獨立接口,
(1)List下有ArrayList,Vector,LinkedList
(2)Set下有HashSet,LinkedHashSet,TreeSet
(2)Map下有Hashtable,LinkedHashMap,HashMap,TreeMap
注意:Queue接口與List、Set同一級別,都是繼承了Collection接口。LinkedList既可以實現Queue接口,也可以實現List接口.Queue接口窄化了對LinkedList的方法的訪問權限(即在方法中的參數類型如果是Queue時,就完全只能訪問Queue接口所定義的方法 了,而不能直接訪問 LinkedList的非Queue的方法)。
2、list詳解:List 存儲有序,可重復
(1)ArrayList解析
1)ArrayList的實現原理:ArrayList繼承AbstractList類,實現了List和RandomAccess,Cloneable, Serializable接口,底層是基於動態的數組。底層使用數組實現,默認初始容量為10.當超出后,會自動擴容為原來的1.5倍,即自動擴容機制。List list = Collections.synchronizedList(new ArrayList(...))即可線程安全。源碼解析如下。
2)ArrayList的優缺點
a、優點: 底層數據結構是數組,查詢快,增刪慢。
b、缺點: 線程不安全,效率高
(2)LinkedList解析
1)LinkedList的實現原理:LinkedList繼承AbstractList類,實現了List,Serializable,Queue接口,LinkedList是通過雙向鏈表去實現的,既然是鏈表實現那么它的隨機訪問效率比ArrayList要低,順序訪問的效率要比較的高。每個節點都有一個前驅(之前前面節點的指針)和一個后繼(指向后面節點的指針)。源碼解析如下。
2)ArrayList的優缺點
a、優點: 底層數據結構是鏈表,查詢慢,增刪快。
b、缺點: 線程不安全,效率高
(3)Vector解析
1)Vector實現原理:在ArrayList中每個方法中添加了synchronized關鍵字來保證同步。
2)Vector的優缺點
a、優點: 底層數據結構是數組,查詢快,增刪慢。
b、缺點: 線程安全,效率低
(4)三種list的選擇(元素可重復):要安全?
是:Vector
否:ArrayList或者LinkedList
查詢多?:ArrayList
增刪多?:LinkedList
知道要用List,但是不知道是哪個List,就用ArrayList。
ArrayList是基於動態的數組的數據結構 LinkedList是基於鏈表的數據結構
3、Set存儲無序,唯一
set保證里面元素的唯一性其實是靠兩個方法,一是equals()和hashCode()方法先是判斷set集合中是否有與新添加數據的hashcode值一致的數據,如果有,那么將再進行第二步調用equals方法再進行一次判斷,假如集合中沒有與新添加數據hashcode值一致的數據,那么將不調用eqauls方法。
(1)HashSet:底層數據結構是哈希表。(無序,唯一)
使用Set集合都是需要去掉重復元素的, 如果在存儲的時候逐個equals()比較, 效率較低,哈希算法提高了去重復的效率, 降低了使用equals()方法的次數,HashSet調用add()方法存儲對象的時候, 先調用對象的hashCode()方法得到一個哈希值, 然后在集合中查找是否有哈希值相同的對象,如果沒有哈希值相同的對象就直接存入集合,如果有哈希值相同的對象, 就和哈希值相同的對象逐個進行equals()比較,比較結果為false就存入, true則不存
(2)LinkedHashSet:底層數據結構是鏈表和哈希表。(FIFO插入有序,唯一),由鏈表保證元素有序,由哈希表保證元素唯一
1)TreeSet:底層數據結構是紅黑樹(唯一,有序);利用自然排序和比較器排序;根據比較的返回值是否是0來決定來保證元素的唯一性。
(3)Set的選擇(元素唯一):
排序?
是:TreeSet或LinkedHashSet
否:HashSet
知道要用Set,但是不知道是哪個Set,就用HashSet。
4、Map接口:Map接口有三個比較重要的實現類,分別是HashMap、HashTable和TreeMap,LinkedHashMap。
(1)TreeMap,HashMap,HashTable的區別
1)TreeMap是有序的,HashMap和HashTable是無序的。
2)Hashtable的方法是同步的,HashMap的方法不是同步的。這是兩者最主要的區別。
3)Hashtable是線程安全的,HashMap不是線程安全的。
4)HashMap效率較高,Hashtable效率較低。查看Hashtable的源代碼就可以發現,除構造函數外,Hashtable的所有 public 方法聲明中都有 synchronized關鍵字,而HashMap的源碼中則沒有。
5)Hashtable不允許null值,HashMap允許null值(key和value都允許)
6)父類不同:Hashtable的父類是Dictionary,HashMap的父類是AbstractMap
(2)LinkedHashMap怎么實現有序的?
LinkedHashMap內部維護了一個單鏈表,有頭尾節點,同時LinkedHashMap節點Entry內部除了繼承HashMap的Node屬性,還有before 和 after用於標識前置節點和后置節點。可以實現按插入的順序或訪問順序排序。
(3)TreeMap怎么實現有序的?
TreeMap是按照Key的自然順序或者實現的Comprator接口的比較函數的順序進行排序,內部是通過紅黑樹來實現。所以要么key所屬的類實現Comparable接口,或者自定義一個實現了Comparator接口的比較器,傳給TreeMap用於key的比較。
5、HashMap解析
(1)JDK1.8中HashMap
1)JDK1.8中HashMap的實現原理(數組+鏈表紅黑樹):數組存儲的元素是一個Entry類,Entry類有三個數據域,key、value(鍵值對),next(指向下一個Entry)
2)HashMap如何設定初始容量大小:一般如果new HashMap() 不傳值,默認大小是16,負載因子是0.75, 如果自己傳入初始大小k,初始化大小為大於k的 2的整數次方,例如如果傳10,大小為16。負載因子為 0.75。Map 在使用過程中不斷的往里面存放數據,當數量達到了16 * 0.75 = 12 就需要將當前 16 的容量進行擴容
3)HashMap的哈希函數如何設計(如何求hash值):hash函數是先拿到通過key的hashcode,是32位的int值,然后讓hashcode的高16位和低16位進行異或操作即key.hashCode()^(key.hashCode()>>>6)。這樣可以盡可能降低hash碰撞,使hash表越分散越好;同時算法一定要盡可能高效,因為這是高頻操作, 因此采用位運算;
4)為什么采用hashcode的高16位和低16位異或能降低hash碰撞?hash函數能不能直接用key的hashcode?
因為key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。int值范圍為-2147483648~2147483647,前后加起來大概40億的映射空間。只要哈希函數映射得比較均勻松散,一般應用是很難出現碰撞的。但問題是一個40億長度的數組,內存是放不下的。如果HashMap數組的初始大小才16,用之前需要對數組的長度取模運算,得到的余數才能用來訪問數組下標。
5)如何通過hash值定位元素的位置:並不是對hash表的長度取余而使用了位運算來得到位置下標,由key的哈希值對數組的長度位運算得到即h & (length-1)
6)為什么用位運算定位hash以及HashMap的擴容都是以2的次方來進行:假設當前table的length是15,二進制表示為1111,那么length-1就是1110,此時有兩個hash值為8和9的key需要計算索引值,計算過程如下:
8的二進制表示:1000
8&(length-1)= 1000 & = 1000,索引值即為8;
9的二進制表示:1001
9&(length-1)= 1001 & 1110 = 1000,索引值也為8;
觀察發現8和9有相同的索引值,即兩個hash值為8和9的key會定位到數組中的同一個位置上形成鏈表,這就產生了碰撞,降低了查詢的效率,HashMap的初始大小和擴容都是以2的次方來進行的,換句話說length-1換成二進制永遠是全部為1,比如容量為16,則length-1為1111,大家知道位運算的'&'規則是兩個1才得1,遇0得0,也就是說length-1中的某一位為1,則對應位置的計算結果才取決於h中的對應位置(h中對應位取0,對應位結果為0,h對應位取1,對應位結果為1。這樣就有兩個結果),但是如果length-1中某一位為0,則不論h中對應位的數字為幾,對應位結果都是0,這樣就讓兩個h取到同一個結果,這就是hash沖突了,恰恰length-1又是全部為1的數,所以結果自然就將hash沖突最小化了。
7)h%length與h&(length-1)得到的結果其實是一個值,但是為什么hashmap中要用后者呢?
a、length(2的整數次冪)的特殊性導致了length-1的特殊性(二進制全為1)
b、位運算快於十進制運算,hashmap擴容也是按位擴容,所以相比較就選擇了后者
8)兩個不同key經過key.hashCode()&(length-1)計算后得到相同的數組下標后,如何操作:hashmap在插入元素的時候,會首先檢查這個位置上有沒有元素,如果已經有了元素,那么就把這個新插入的Entry的next指向本來這個位置上的元素的地址,然后再插入這個位置,這也就是為什么插入多個相同的key的value時,這個位置的value始終是最后插入的那個元素的值。
9)jdk1.8的HashMap的put方法:
a、判斷數組是否為空,為空進行初始化;
b、不為空,計算 k 的 hash 值,通過(n - 1) & hash計算應當存放在數組中的下標 index;
c、查看 table[index] 是否存在數據,沒有數據就構造一個Node節點存放在 table[index] 中;
d、存在數據,說明發生了hash沖突(存在二個節點key的hash值一樣), 繼續判斷key是否相等,相等,用新的value替換原數據;
e、如果不相等,判斷當前節點類型是不是樹型節點,如果是樹型節點,創造樹型節點插入紅黑樹中;
f、如果不是樹型節點,創建普通Node加入鏈表中;判斷鏈表長度是否大於 8, 大於的話鏈表轉換為紅黑樹;
g、插入完成之后判斷當前節點數是否大於閾值,如果大於開始擴容為原數組的2倍。
10)jdk1.8的HashMap的get方法:
a、首先將 key hash 之后取得所定位的桶。
b、如果桶為空則直接返回 null 。
c、否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否為查詢的 key,是就直接返回 value。
d、如果第一個不匹配,則判斷它的下一個是紅黑樹還是鏈表。
e、紅黑樹就按照樹的查找方式返回值。
f、不然就按照鏈表的方式遍歷匹配返回值。
11)jdk1.8中HashMap的三點主要的優化:
a、數組+鏈表改成了數組+鏈表或紅黑樹;1.8使用紅黑樹:防止發生hash沖突,鏈表長度過長,將時間復雜度由O(n)降為O(logn);
b、鏈表的插入方式從頭插法改成了尾插法,是插入時,若數組位置上已經有元素,1.7利用頭插法,1.8遍歷鏈表,將元素放置到鏈表的最后; 因為1.7頭插法擴容時,頭插法會使鏈表發生反轉,多線程環境下會產生環
c、擴容的時候1.7需要對原數組中的元素進行重新hash定位在新數組的位置,1.8采用更簡單的判斷邏輯,位置不變;
d、在插入時,1.7先判斷是否需要擴容,再插入,1.8先進行插入,插入完成再判斷是否需要擴容;
12)擴容的時候為什么1.8 不用重新hash就可以直接定位原節點在新數據的位置呢?
由於擴容是擴大為原數組大小的2倍,用於計算數組位置的掩碼僅僅只是高位多了一個1,擴容前長度為16,用於計算(n-1) & hash 的二進制n-1為0000 1111,擴容為32后的二進制就高位多了1,為0001 1111。因為是& 運算,1和任何數 & 都是它本身,那就分二種情況:hash比(length-1)大和比(length-1)小。如下圖所示。
13)鏈表轉紅黑樹的閾值是8,為什么紅黑樹轉鏈表的閾值是6?
在hash函數設計合理的情況下,發生hash碰撞8次的幾率為百萬分之6,概率說話。因為8夠用了,至於為什么轉回來是6,因為如果hash碰撞次數在8附近徘徊,會一直發生鏈表和紅黑樹的互相轉化,為了預防這種情況的發生,設置為6。
14)Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以實現線程安全的Map
a、HashTable是直接在操作方法上加synchronized關鍵字,鎖住整個數組,鎖粒度比較大,
b、Map m = Collections.synchronizedMap(new HashMap());
c、ConcurrentHashMap相比HashTable降低了鎖粒度,並發度提高。jdk1.7使用分段鎖實現ConcurrentHashMap線程安全,jdk1.8使用CAS實現ConcurrentHashMap線程安全。
6、ConcurrentHashMap
1)ConcurrentHashMap在jdk1.7是如何實現的:jdk1.7中是采用Segment 數組+ HashEntry +成員變量用volatile修飾+ ReentrantLock的方式進行實現的。其中segment繼承ReentrantLock;另外成員變量使用volatile 修飾,免除了指令重排序,同時保證內存可見性,不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理。理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組長度)的線程並發。每當一個線程占用鎖訪問一個 Segment 時,不會影響到其他的 Segment。
注意:volatile關鍵字對於基本類型的修改可以在隨后對多個線程的讀保持一致,但是對於引用類型如數組僅僅保證引用的可見性,但並不保證引用內容的可見性。
2)ConcurrentHashMap在jdk1.8是如何實現的?
數據結構類似於hashmap,放棄了Segment臃腫的設計,取而代之的是采用Node數組+ CAS + Synchronized來保證並發安全進行實現。
jdk1.8 在jdk1.7 的數據結構上做了大的改動,采用紅黑樹之后可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對 synchronized 優化是很到位的。
3)jdk1.8的ConcurrentHashMap的put操作:
a、根據 key 計算出 hashcode ;
b、判斷是否需要進行初始化。
c 、定位出當前 key的 Node,如果為空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
d、如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
e、如果都不滿足,則利用 synchronized 鎖寫入數據。
f、如果數量大於 TREEIFY_THRESHOLD 則要轉換為紅黑樹。
4)jdk1.8的ConcurrentHashMap 的get操作
a、根據計算出來的 hashcode 尋址,如果就在桶上那么直接返回值。
b、如果是紅黑樹那就按照樹的方式獲取值。
c、就不滿足那就按照鏈表的方式遍歷獲取值。
由於 HashEntry 中的value屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值。get 方法是非常高效的,因為整個過程都不需要加鎖。