Java容器基本要點

Java容器基本要點

可將java容器類庫劃分爲兩個不同的概念:

1)Collection:一個獨立元素的序列,這些元素都服從一條或多條規則。List按照插入順序保存元素,set不插入重複元素。Queue按照隊列規則確定對象產生順序。

2)Map:一組成對的<key,value>對象,可以使用鍵值key來查找值。

1.  List

List可以將元素維護在特定的序列中。

有兩種類型的List:

1>  基本的ArrayList,它便於處理隨機訪問元素,但是在List的中間插入和移除元素時較慢。

2>  LinkedList,它通過代價較低的在List中間進行的插入和刪除操作,提供了優化的順序訪問。不善於處理隨機訪問。LinkedList還添加了可以使其用作棧、隊列或雙端隊列訪問的方法。

 

兩者都是按照被插入的順序保存元素。
而ArrayList 採用的是數組形式來保存對象的,這種方式將對象放在連續的位置中,所以最大的缺點就是插入刪除時非常麻煩。
LinkedList 採用的將對象存放在獨立的空間中,即是基於鏈表的數據結構,而且在每個空間中還保存下一個鏈接的索引,但是缺點就是查找非常麻煩,要從第一個索引開始。

ArrayList和LinkedList在性能上各有優缺點,都有各自所適用的地方,總的說來可以描述如下:

1. 對ArrayList和LinkedList而言,在列表末尾增加一個元素所花的開銷都是固定的。對ArrayList而言,主要是在內部數組中增加一項,指向所添加的元素,偶爾可能會導致對數組重新進行分配;而對LinkedList而言,這個開銷是統一的,分配一個內部Entry對象。

2.在ArrayList的中間插入或刪除一個元素意味着這個列表中剩餘的元素都會被移動;而在LinkedList的中間插入或刪除一個元素的開銷是固定的。

3.LinkedList不支持高效的隨機元素訪問。

4.ArrayList的空間浪費主要體現在在list列表的結尾預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗相當的空間

可以這樣說:當操作是在一列數據的後面添加數據而不是在前面或中間,並且需要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能;當你的操作是在一列數據的前面或中間添加或刪除數據,並且按照順序訪問其中的元素時,就應該使用LinkedList了。

2.  Set

在Java中使用Set,可以方便地將需要的類型以集合類型保存在一個變量中.主要應用在顯示列表,Set是一個不包含重複元素的collection。更確切地講,set 不包含滿足 e1.equals(e2) 的元素對 e1 和 e2,並且最多包含一個 null元素。正如其名稱所暗示的,此接口模仿了數學上的 set 抽象。

HashSet,TreeSet,EnumSet三個類實現了Set接口:

2.1 HashSet

HashSet具有的特點:

1 HashSet使用散列函數實現,不能保證元素的排列順序,順序可能發生變化。

2 HashSet不是同步的,如果多個線程同時訪問一個Set集合,如果多個線程同時訪問一個HashSet,如果有2條或者以上的線程修改了HashSet集合時,必須通過代碼來保證其是同步的(TreeSet和EnumSet也一樣)。

3 集合元素可以是null。

HashSet判斷兩個元素相等是的標準是兩個對象通過equals方法比較相等,並且兩個對象的hashCode方法返回的值也相等。

當向HashSet中添加可變對象是,必須非常的小心,如果修改HashSet中的對象時,有可能導致該對象與集合中的其他對象相等,從而導致HashSet無法準確訪問該對象。

HashSet的子對象LinkedHashSet使用鏈表維護元素次序,使元素以插入順序保存,性能略低於HashSet。

2.2 TreeSet

TreeSet是SortedSet接口的唯一實現,可以確保元素處於排序狀態。與HashSet集合採用hash算法來決定元素的存儲位置不同,TreeSet集合採用紅黑樹的數據結構來對元素進行排序,分自然排序和定製排序倆種,默認的情況下采用自然排序。

試圖把一個對象添加到TreeSet時,該對象的類必須實現Comparable接口,否則出現異常(添加第一個時沒事,添加第二個時,TreeSet調 用該對象的CompareTo(Object obj)方法時引發異常),而且向TreeSet添加的對象應該屬於同一個類的對象,否則也會引發異常。

一些常用類已經實現了Comparable接口,比如BigDecimal,BigInteger,

Character,Boolean(true>false),String,Date,Time等。

對於TreeSet而言,判斷兩個對象不相等的標準是:equals方法返回false,或compareTo方法沒有返回0,即使兩個對象是同一個對象也會當做兩個對象處理。所有當重寫一個須放入TreeSet的類的equals方法時,應該保證與compareTo方法有一致的結果。

如果向TreeSet中添加一個可變對象後,並且後面的程序修改了該對象的屬性,導致它與其他對象的大小發生了變化,但TreeSet不會再次調整 它們的順序,甚至導致保存的這兩個對象通過equals返回true,而compareTo確返回0,所有推薦HashSet和TreeSet集合中只放入不可變對象。

2.3  EnumSet

EnumSet是一個專門爲枚舉類設計的集合,內部以向量的方式存儲,佔用內存很少,運行效率很高。

EnumSet不允許加入null元素,否則會出現異常。當試圖複製一個Collection集合裏的元素來創建EnumSet集合時,必須保證Collection集合裏的所有元素都是同一個枚舉類的枚舉值。

3 Map

Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。在此引入負載因子的概念。

負載因子:尺寸/容量。在java語言中,通過負載因子(load factor)來決定何時對散列表進行再散列。例如:如果負載因子是0.75,當散列表中已經有75%的位置已經放滿,那麼將進行散列。負載因子越高(越接近1.0),內存的使用率越高,元素的尋找時間越長。負載因子越低(越接近0.0),元素的尋找時間越短,內存浪費越多。
如下是基本的Map實現。
1)HashMap Map是基於散列表的實現(取代了HashTable)。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設置容量和負載因子,以調整容量的性能。
2)LinkedHashMap 類似於HashMap,但是迭代遍歷時,取得“鍵值對”的順序是其插入順序,或者是最近最少使用的順序。只比HashMap慢一點;而在迭代訪問時反而更快,因爲它使用鏈表維護內部次序。
3)TreeMap 基於紅黑樹的實現。查看“鍵值對”或“鍵”時,它們會被排序(次序由Comparator或Comparable決定)。TreeMap的特點在於,所得到的結果是經過排序的。TreeMap是唯一帶有subMap()方法的Map,它可以返回一個子樹。
4)WeakHashMap 弱鍵映射,允許釋放映射所指向的對象,這是爲解決某類特殊問題而設計的。如果映射之外沒有引用指向某個“鍵”,則此“鍵”可以被垃圾收集器回收。
5)ConcurrentHashMap 一種線程安全的Map,它不涉及同步加鎖。
6)IdentityHashMap 使用==代替equals()對“鍵”進行比較的散列映射,當且僅當 (k1==k2) 時,才認爲兩個鍵 k1 和 k2 相等,因此其允許有相同的Key值。

3.1 Hashtable類    

  Hashtable繼承Map接口,實現一個key-value映射的哈希表。任何非空(non-null)的對象都可作爲key或者value。       

Hashtable通過initial capacity(初始容量)和load factor(負載因子)兩個參數調整性能。通常缺省的load factor  0.75較好地實現了時間和空間的均衡。增大load  factor可以節省空間但相應的查找時間將增大,這會影響像get和put這樣的操作。 

  由於作爲key的對象將通過計算其散列函數來確定與之對應的value的位置,因此任何作爲key的對象都必須實現hashCode和equals方法。hashCode和equals方法繼承自根類Object,如果你用自定義的類當作key的話,要相當小心,按照散列函數的定義,如果兩個對象相同,即obj1.equals(obj2)=true,則它們的hashCode必須相同,但如果兩個對象不同,則它們的hashCode不一定不同,如果兩個不同對象的hashCode相同,這種現象稱爲衝突,衝突會導致操作哈希表的時間開銷增大,Map處理這些衝突的方法是在索引位置處插入一個鏈接列表,並簡單地將元素添加到此鏈接列表。所以儘量定義好的hashCode()方法,能加快哈希表的操作。    

  如果相同的對象有不同的hashCode,對哈希表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時複寫equals方法和hashCode方法,而不要只寫其中一個。      

Hashtable是同步的。       

3.2 HashMap類    

  HashMap和Hashtable類似,不同之處在於HashMap是非同步的,並且允許null,即null value和null key。Hashtable的應用非常廣泛,HashMap是新框架中用來代替Hashtable的類,也就是說建議使用HashMap,不要使用Hashtable。

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

3.3HashTable和HashMap區別

1> 繼承不同。

public class Hashtable extends Dictionary implements Map
public class HashMap  extends AbstractMap implements Map

2> Hashtable 中的方法是同步的,而HashMap中的方法在缺省情況下是非同步的。在多線程併發的環境下,可以直接使用Hashtable,但是要使用HashMap的話就要自己增加同步處理了。

3> Hashtable中,key和value都不允許出現null值。在HashMap中,null可以作爲鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值爲null。當get()方法返回null值時,即可以表示 HashMap中沒有該鍵,也可以表示該鍵所對應的值爲null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

4> 兩個遍歷方式的內部實現上不同。Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。

5>  哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值。

6> Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。HashTable中hash數組默認大小是11增加的方式是 old*2+1HashMap中hash數組的默認大小是16,而且一定是2的指數。 

4 Queue

Queue 接口與List、Set同一級別,都是繼承了Collection接口。除了併發應用,Queue在java SE5 中僅有的兩個實現是LinkedList和PriorityQueue。LinkedList實現了Queue接口。Queue接口窄化了對LinkedList的方法的訪問權限(即在方法中的參數類型如果是Queue時,就完全只能訪問Queue接口所定義的方法 了,而不能直接訪問 LinkedList的非Queue的方法),以使得只有恰當的方法纔可以使用。BlockingQueue 繼承了Queue接口。

BlockingQueue成員詳細介紹
1. ArrayBlockingQueue
   基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象,這是一個常用的阻塞隊列,除了一 個定長數組外,ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。
  ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,由此也意味着兩者無法真正並行運行,這點尤其不同於LinkedBlockingQueue;按照實現原理來分析,ArrayBlockingQueue完全可以採用分離鎖,從而實現生產者和消費者操作的完全並行運行。Doug Lea之所以沒這樣去做,也許是因爲ArrayBlockingQueue的數據寫入和獲取操作已經足夠輕巧,以至於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全佔不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue間還有一個明顯的不同之處在於,前者在插入或刪除元素時不會產生或銷燬任 何額外的對象實例,而後者則會生成一個額外的Node對象。這在長時間內需要高效併發地處理大批量數據的系統中,其對於GC的影響還是存在一定的區別。而在創建ArrayBlockingQueue時,我們還可以控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。

2.LinkedBlockingQueue

  基於鏈表的阻塞隊列,同ArrayListBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者立即返回;只有當隊列緩衝區達到最大值緩存容量時 (LinkedBlockingQueue可以通過構造函數指定該值),纔會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於同樣的原理。而LinkedBlockingQueue之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。
作爲開發者,我們需要注意的是,如果構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。
ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通也是最常用的阻塞隊列,一般情況下,在處理多線程間的生產者消費者問題,使用這兩個類足以。

3. DelayQueue
   DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue是一個沒有大小限制的隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。
使用場景:
  DelayQueue使用場景較少,但都相當巧妙,常見的例子比如使用一個DelayQueue來管理一個超時未響應的連接隊列。
4. PriorityBlockingQueue
  基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。

5. SynchronousQueue
 一種無緩衝的等待隊列,類似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,如果一方沒有找到合適的目標,那麼對不起,大家都在集市等待。相對於有緩衝的BlockingQueue來說,少了一箇中間經銷商的環節(緩衝區),如果有經銷商,生產者直接把產品批發給經銷商,而無需在意經銷商最終會將這些產品賣給那些消費者,由於經銷商可以庫存一部分商品,因此相對於直接交易模式,總體來說採用中間經銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因爲經銷商的引入,使得產品從生產者到消費者 中間增加了額外的交易環節,單個產品的及時響應性能可能會降低。
  聲明一個SynchronousQueue有兩種不同的方式,它們之間有着不太一樣的行爲。公平模式和非公平模式的區別:
  如果採用公平模式:SynchronousQueue會採用公平鎖,並配合一個FIFO隊列來阻塞多餘的生產者和消費者,從而體系整體的公平策略;
  但如果是非公平模式(SynchronousQueue默認):SynchronousQueue採用非公平鎖,同時配合一個LIFO隊列來 管理多餘的生產者和消費者,而後一種模式,如果生產者和消費者的處理速度有差距,則很容易出現飢渴的情況,即可能有某些生產者或者是消費者的數據永遠都得不到處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章