面試考點系列【集合】(一)

弱冠季首發表於2018-03-28

集合是面試的一個重災區,每面必問!!!

廢話不多說直奔主題,下面就針對具體的問題進行解釋。

一、常用的集合有哪些?

     常用的集合主要有List,Set和Map三大類。其中List和Set是實現了Collection介面,Map另立門戶。

二、既然提到了Collection 那它和Collections有什麼區別?

    Collection集合類的上級介面,繼承與他有關的介面主要有List和Set。

    Collections是針對集合類的一個幫助類,他提供一系列靜態方法實現對各種集合的搜尋、排序、執行緒安全等操作。

三、Collections常用的方法有哪些?

1) 排序(Sort)

Collections.sort(list);複製程式碼
使用sort方法可以根據元素的自然順序 對指定列表按升序進行排序。列表中的所有元素都必須實現 Comparable 介面。

2) 混排(Shuffling)

Collections.Shuffling(list)複製程式碼

混排演算法所做的正好與 sort 相反: 它打亂在一個 List 中可能有的任何排列的蹤跡。也就是說,基於隨機源的輸入重排該 List, 這樣的排列具有相同的可能性(假設隨機源是公正的)。這個演算法在實現一個碰運氣的遊戲中是非常有用的。例如,它可被用來混排代表一副牌的 Card 物件的一個 List 。另外,在生成測試案例時,它也是十分有用的。


3) 反轉(Reverse)

Collections.reverse(list)複製程式碼

使用Reverse方法可以根據元素的自然順序 對指定列表按降序進行排序。

4) 替換所以的元素(Fill)

Collections.fill(li,"aaa");複製程式碼

使用指定元素替換指定列表中的所有元素。


5) 拷貝(Copy)

Collections.copy(list,li): 後面一個引數是目標列表 ,前一個是源列表複製程式碼

用兩個引數,一個目標 List 和一個源 List, 將源的元素拷貝到目標,並覆蓋它的內容。目標 List 至少與源一樣長。如果它更長,則在目標 List 中的剩餘元素不受影響。

6) 返回Collections中最小元素(min)

Collections.min(list)複製程式碼

根據指定比較器產生的順序,返回給定 collection 的最小元素。collection 中的所有元素都必須是通過指定比較器可相互比較的

7) 返回Collections中最小元素(max)

Collections.max(list)複製程式碼

根據指定比較器產生的順序,返回給定 collection 的最大元素。collection 中的所有元素都必須是通過指定比較器可相互比較的

8) lastIndexOfSubList

int count = Collections.lastIndexOfSubList(list,li);複製程式碼

返回指定源列表中最後一次出現指定目標列表的起始位置

9) IndexOfSubList

int count = Collections.indexOfSubList(list,li);複製程式碼

返回指定源列表中第一次出現指定目標列表的起始位置

10) Rotate

Collections.rotate(list,-1);如果是負數,則正向移動,正數則方向移動複製程式碼

根據指定的距離迴圈移動指定列表中的元素


四、List中常用的以及底層實現

常用的List有ArrayList、LinkedList;

ArrayList底層是一個陣列佇列,相當於 動態陣列。與Java中的陣列(Array)相比,它的容量能動態增長,還實現了RandomAccess介面。

基於這種底層實現,所以進行插入刪除的時候,需要進行移動,所以插入效率比較低。但是查詢和遍歷的時候效率就比較高。比較適合用foreach這種遍歷方式

LinkedList底層是一個連結串列表,還實現了Deque介面。

基於這種底層實現,所以進行插入的時候只需要修改連結串列指標,所以插入效率相對高一些。但是查詢的時候需要一個一個指標指過去所以效率比較低。比較適合用iterator迭代器進行遍歷。因為他還實現了雙向佇列介面,所以可以用他實現佇列和棧。

五、Map中常用的以及底層實現

常用的Map有HashMap,LinkedHashMap,TreeMap;

HashMap 

HashMap 是一個雜湊表,它儲存的內容是鍵值對(key-value)對映。

是一個最常用的Map,它根據鍵的HashCode 值儲存資料,根據鍵可以直接獲取它的值,具有很快的訪問速度。HashMap最多隻允許一條記錄的鍵為Null;允許多條記錄的值Null;

HashMap不支援執行緒的同步,即任一時刻可以有多個執行緒同時寫HashMap;可能會導致資料的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

LinkedHashMap

LinkedHashMap也是一個HashMap,但是內部維持了一個雙向連結串列,可以保持順序,可以保證插入順序有序。

TreeMap 

TreeMap基於紅黑樹(Red-Black tree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。

TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。


六、HashMap為什麼執行緒不安全

HashMap 的例項有兩個引數影響其效能:“初始容量” 和 “載入因子”。容量 是雜湊表中桶的數量,初始容量 只是雜湊表在建立時的容量。載入因子 是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。
通常,預設載入因子是 0.75, 這是在時間和空間成本上尋求一種折衷。載入因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以載入因子,則不會發生 rehash 操作。

HashMap的實現裡沒有鎖的機制,因此它是執行緒不安全的。

如果在HashMap內部加鎖讓它變成執行緒安全,這樣會增加單執行緒訪問的資源消耗,即使沒有多執行緒訪問,也要每次檢查、加鎖、解鎖。(HashTable執行緒安全就是因為內部加鎖)

執行緒不安全的表現1:

多執行緒情況下,兩個執行緒A(取資料)B(存資料),如果A執行緒在剛到達獲取的動作還沒執行的時候,執行緒執行的機會又跳到執行緒B,此時執行緒B又對modelHashMap賦值,這樣就會導致Map中存放的值一直丟失。簡單說就是兩個執行緒在同一個位置新增資料,後面新增的資料就覆蓋住了前面新增的。

執行緒不安全的表現2:

如果在預設情況下,一個HashMap的容量為16,載入因子為0.75,那麼閥值就是12,所以在往HashMap中put的值到達12時,它將自動擴容兩倍,如果兩個執行緒同時遇到HashMap的大小達到12的倍數時,就很有可能會出現在將oldTable轉移到newTable的過程中遇到問題,從而導致最終的HashMap的值儲存異常

執行緒不安全的表現3:

 在多執行緒環境中,使用HashMap進行put操作時會引起死迴圈,導致CPU使用接近100%。當HashMap擴容時需要將原連結串列資料的陣列拷到新的連結串列陣列中,在進行拷貝的過程中會形成環鏈造成死迴圈。

七、ConcurrentHashMap為什麼就執行緒安全?

ConcurrentHashMap採用了分段鎖的設計,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭。相比於對整個Map加鎖的設計(HashTable的設計),分段鎖大大的提高了高併發環境下的處理能力。但同時,由於不是對整個Map加鎖,導致一些需要掃描整個Map的方法(如size(), containsValue())需要使用特殊的實現,另外一些方法(如clear())甚至放棄了對一致性的要求。

ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。ConcurrentHashMap中的HashEntry相對於HashMap中的Entry有一定的差異性:HashEntry中的value以及next都被volatile修飾,這樣在多執行緒讀寫過程中能夠保持它們的可見性

和JDK6一樣,ConcurrentHashMap的put方法被代理到了對應的Segment(定位Segment的原理之前已經描述過)中。與JDK6不同的是,JDK7版本的ConcurrentHashMap在獲得Segment鎖的過程中,做了一定的優化 - 在真正申請鎖之前,put方法會通過tryLock()方法嘗試獲得鎖,在嘗試獲得鎖的過程中會對對應hashcode的連結串列進行遍歷,如果遍歷完畢仍然找不到與key相同的HashEntry節點,則為後續的put操作提前建立一個HashEntry。當tryLock一定次數後仍無法獲得鎖,則通過lock申請鎖。

需要注意的是,由於在併發環境下,其他執行緒的put,rehash或者remove操作可能會導致連結串列頭結點的變化,因此在過程中需要進行檢查,如果頭結點發生變化則重新對錶進行遍歷。而如果其他執行緒引起了連結串列中的某個節點被刪除,即使該變化因為是非原子寫操作(刪除節點後連結後續節點呼叫的是Unsafe.putOrderedObject(),該方法不提供原子寫語義)可能導致當前執行緒無法觀察到,但因為不影響遍歷的正確性所以忽略不計。

之所以在獲取鎖的過程中對整個連結串列進行遍歷,主要目的是希望遍歷的連結串列被CPU cache所快取,為後續實際put過程中的連結串列遍歷操作提升效能。

在獲得鎖之後,Segment對連結串列進行遍歷,如果某個HashEntry節點具有相同的key,則更新該HashEntry的value值,否則新建一個HashEntry節點,將它設定為連結串列的新head節點並將原頭節點設為新head的下一個節點。新建過程中如果節點總數(含新建的HashEntry)超過threshold,則呼叫rehash()方法對Segment進行擴容,最後將新建HashEntry寫入到陣列中。

put方法中,連結新節點的下一個節點(HashEntry.setNext())以及將連結串列寫入到陣列中(setEntryAt())都是通過Unsafe的putOrderedObject()方法來實現,這裡並未使用具有原子寫語義的putObjectVolatile()的原因是:JMM會保證獲得鎖到釋放鎖之間所有物件的狀態更新都會在鎖被釋放之後更新到主存,從而保證這些變更對其他執行緒是可見的。

八、為什麼concurrentHashMap的get方法不需要加鎖?

因為concurrentHashMap中的HashEntry中的value以及next都被volatile修飾,在讀操作的時候不需要對value進行修改,寫操作的時候加鎖保證了不會被其他執行緒進行修改,然後volatile又可以保證資料實時,所以不需要在進行加鎖操作。但是當值為Null的時候還是會加鎖重讀的。


相關文章