Java集合學習筆記


 
在Java中,我們經常聽到Collections框架、Collection類以及Collections類。這三者名字相似,但是從概念上講卻是不同的。Collections框架泛指Java中用於存儲和操作集合的類庫總和,其中包括了List、Set和Map等。但是在具體實現上,由於Map中裝的是Key-Value的鍵值對元素,其接口形式和其他(比如List)接口不一樣,因此在Java中Map被區分對待了。總的來看,位於Java Collections框架中最頂層的接口有兩個,Collection類和Map類,此時整個Java Collections框架的類層級如下:
 
 
 
 
Java中存在不少泛化的方法能夠直接對Collection接口進行操作,而不用關心具體的實現類是什么,比如在查找一個集合中的最大值元素的時候——即實現一個max()方法,此時我們並不需要知道這個集合具體的實現類是ArrayList還是HashSet,而只需要知道這是一個Collection集合即可。另外,這個max()方法對於所有具體實現類來說其實原理是一樣的,因此沒有必要讓每一個集合實現類自己再去實現一份。基於此,Java引入了一個Collections工具類來輔助實現這些方法,比如操作集合和創建集合等。由於名字和Collection相似,Collections通常給程序員們帶來誤解,這一點需要注意。當然,Java 8中引入了“默認方法”(Default Method)的技術,使得理論上諸如max()這樣的方法可以直接實現在Collection接口上。 
 
集合類的選用
在平時開發中,程序員們使用最多了莫過於ArrayList、HashSet和HashMap了。在多數情況下,這三個實現類已經能夠滿足我們的大部分需求,但是如果稍加分析我們可能會發現另外一些集合實現類能更好的滿足我們的需要。比如,當你需要一個有順序的Set時,可以考慮選用LinkedHashSet而不是HashSet。關於集合類是選用,可以參考下圖:
 
 
 
 
equals()和hashCode()方法
工作過一段時間的Java程序員基本上都知道equals()和hashCode()之間存在着以下契約關系:
  1. 如果兩個對象通過equals()方法判斷出是相等的,那么他們hashCode也應該相等;
  2. 如果兩個對象通過equals()方法判斷不等,那么他們hashCode可以相等,也可以不等。

如果沒有實現以上兩條(特別是第1條),在使用Java某些集合時(特別是HashSet和HashMap),你將得不到想要的結果,甚至造成嚴重的內存泄漏問題。要搞明白里面的原由,我們還得從HashMap的內部實現開始說起。

 

HashMap在存儲鍵值對(Key-Value Pair)時,首先調用Key對象的hashCode()方法得到一個數字,這個數字對應了該鍵值對在HashMap中的存放位置,每個位置上不僅存放了Value,還存放了Key,即鍵值對(如下圖所示)。我們將存放鍵值對的位置叫做一個Bucket(中文名為“桶”,即用來裝東西的容器,很形象哈),Bucket中維護了一個LinkedList(下圖中的Entries),該LinkedList用於存放實際的鍵值對本身。因此,要通過Key來獲取到相應的Value,Java只需要再次調用Key的hashCode()方法得到該鍵值對在HashMap中的位置,便可以准確快速地獲取到Key對應的Value。因此,即便用於存放的Key和用於獲取的Key是相等的,如果他們的hashCode不等,那么在獲取時便得不到先前存放的准確位置,進而得不到正確的結果。另外,如果Key對象的類沒有實現equals()方法,那么默認情況下Java將使用對象的引用地址來判斷兩個對象的相等性, 而之后我們又很難再次創建出一個和先前引用地址相同的對象,因此便可能出現永遠也獲取不到Value的情況,從而導致內存泄漏。進而我們得到另一個結論:如果一個類的對象將被用於HashMap的Key或者被直接放入Set集合中,那么這個類應該實現equals()方法和hashCode()方法。

 

 

 

注意到上圖中的152號Bucket了嗎?我們發現同時有John Smith和Sandra Dee兩個Key都指向了這個相同的Bucket,即這兩個對象的hashCode相等。這便是equals()和hashCode()契約關系的第2點。Java采用了LinkedList來存放所有Key的hashCode相等的鍵值對。此時在LinkedList中,前一個鍵值對維護了一個指針指向了下一個鍵值對。當之后通過John Smith來獲取Value時,Java首先發現其對應的Bucket中存在着兩個鍵值對,然后Java分別將各個鍵值對中Key對象與所傳來的Key對象相比,如果相等則返回該鍵值對所對應的Value。由此,我們也知道HashMap中為什么需要同時存Key和Value而不是只存Value的原因;同時也知道了契約第2點的來由。事實上,我們可以讓一個類的所有對象都返回相同的hashCode,只是此時如果該類的對象作為Key時,所有的鍵值對都將存放都相同的Bucket位置,每次在獲取的時候都需要做多次的比較,因此會影響HashMap的獲取速度。

 

線程安全性

通常來說,在Java中可以通過兩種方式來實現線程安全,一種是通過Java自帶的並發管控手段(比如使用syncronized關鍵字),另一中是通過創建不可變(Immutable)對象。

 

在很早的時候,Java里面有Vector和HashTable兩個集合對象,他們通過使用syncronized關鍵字實現了線程安全,但是同時也暴露出了很大的性能問題。因此現在基本上沒有人使用了。為了解決Vector和HashTable的性能問題,Java從1.2引入的Collections框架采用了線程不安全的類,比如ArrayList和HashMap都是線程不安全的。當然,為了性能而犧牲了線程安全性也是不可取的,因此Java通過Fail-Fast的Iterator來避免多個線程同時操作集合所帶的線程沖突問題。比如,當一個線程正在遍歷一個集合而另一個線程正在修改該集合時,前者將拋出ConcurrentModificationException。這當然也不是萬全的辦法。

 

另一方面,Java其實也提供了線程安全的封裝類(Wrapper)來實現集合的線程安全性,我們可以通過:

Collections.synchronizedXXX(collection)

來創建線程安全的集合,這里的XXX可以是Collection、List、Map和Set等。對於一個常規的colleciton對象,調用(synchronizedXXX)方法將得到封裝后的線程安全性。

 

集合封裝類同樣采用了syncronized關鍵字來達到線程安全性,並且是在整個集合類上上鎖,這樣也會帶來嚴重的性能問題。為了解決這樣的問題,從Java 5開始引入了並發集合(Concurrent Collections),他們要么采用Immutable集合,要么采用更加精細的鎖控制來達到線程安全的目的,同時又能保證很高的性能。

 

並發集合主要包含三類,一是Copy-On-Write集合,二是Compare-And-Swap集合,三是采用特殊鎖的並發集合。Copy-On-Write集合底層維護的是一個不變的(Immutable)的數組,通過在寫(Write)入集合時重新復制(Copy)一份新的集合來達到線程安全,進而得名Copy-On-Write。Coppy-On-Write集合包括有CopyOnWriteArrayList和CopyOnWriteArraySet等。Compare-And-Swap集合在進行更新的時候,首先維護一個本地拷貝,當執行更新時,比較本地拷貝與原值,如果值相等,則證明在這段時間內還沒有其他線程修改原值,此時立即更新;如果不相等,則重新拷貝原值,再計算,再更新,這樣也到了線程安全的目的。Compare-And-Swap集合包括ConcurrentLinkedQueue和ConcurrentSkipListMap等。第三類是使用特殊鎖的集合,這種集合類並不在整個集合類上上鎖,而是通過在Bucket級別上上鎖,從而達到了對並發的更精細的控制,減少了線程的等待時間,從而提高了並發性能。

 

除了提供並發控制機制外,Java還提供了不可修改的(unmodifiable)集合來保證線程安全性,可以通過:

Collections.unmodifiableXXX(collection)

來創建不同unmodifiable集合。這樣的集合其實也是一個封裝類,對於傳入的正常集合collection,通過對add()等方法拋出UnsupportedOperationException異常來達到不可修改的目的。但是,這樣的集合其實並不達到不可修改目的,因為被其包裝的collection本身依然是可以修改的。

 

為了實現更好的集合不變性,Guava類庫提供了很多Immutable的集合,這些集合是真正不變的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM