Java基礎-Collection類詳解

之前做的一些項目裏也用過一些集合類,但是java中對集合類的具體實現卻不是很瞭解,在網上找了一些資料,覺得這篇文章寫的很全面,於是轉載過來學習一下。

原文鏈接:http://my.oschina.net/xianggao/blog/90189

在Java2中,有一套設計優良的接口和類組成了Java集合框架Collection,使程序員操作批量的數據或對象元素極爲方便。這些接口和類有很多對抽象數據類型操作的API,而這是我們常用的且在數據結構中熟知的,例如Map,Set,List等。並且Java用面向對象的設計對這些數據結構和算法進行了封裝,這就極大的減化了程序員編程時的負擔。程序員可以以這個集合框架爲基礎,定義更高級別的數據抽象,比如棧、隊列和線程安全的集合等,從而滿足自己的需要。

線性表,鏈表,哈希表是常用的數據結構,在進行Java開發時,JDK已經爲我們提供了一系列相應的類來實現基本的數據結構。這些類均在java.util包中。本文試圖通過簡單的描述,向讀者闡述各個類的作用以及如何正確使用這些類。

一、Conllection類介紹

QQ截圖20121118172448

1. Collection接口

Collection是最基本的集合接口,一個Collection代表一組Object,即Collection的元素(Elements)。一些Collection允許相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接繼承自Collection的類,Java SDK提供的類都是繼承自Collection的“子接口”如List和Set

所有實現Collection接口的類都必須提供兩個標準的構造函數:a) 無參數的構造函數用於創建一個空的Collection;b) 有一個Collection參數的構造函數用於創建一個新的Collection,這個新的Collection與傳入的Collection有相同的元素。後一個構造函數允許用戶複製一個Collection。

如何遍歷Collection中的每一個元素?不論Collection的實際類型如何,它都支持一個iterator()的方法,該方法返回一個迭代子,使用該迭代子即可逐一訪問Collection中每一個元素。典型的用法如下:

Iterator it = collection.iterator(); // 獲得一個迭代子 
while(it.hasNext()) { 
      Object obj = it.next(); // 得到下一個元素 
}

由Collection接口派生的兩個接口是List和Set。

主要方法:

boolean add(Object o) 添加對象到集合

boolean remove(Object o) 刪除指定的對象

int size() 返回當前集合中元素的數量

boolean contains(Object o) 查找集合中是否有指定的對象

boolean isEmpty() 判斷集合是否爲空

Iterator iterator() 返回一個迭代器

boolean containsAll(Collection c) 查找集合中是否有集合c中的元素

boolean addAll(Collection c) 將集合c中所有的元素添加給該集合

void clear() 刪除集合中所有元素

void removeAll(Collection c) 從集合中刪除c集合中也有的元素

void retainAll(Collection c) 從集合中刪除集合c中不包含的元素

2. List接口

List是有序的Collection,使用此接口能夠精確的控制每個元素插入的位置。用戶能夠使用索引(元素在List中的位置,類似於數組下標)來訪問List中的元素,這類似於Java的數組。

和下面要提到的Set不同,List允許有相同的元素。除了具有Collection接口必備的iterator()方法外,List還提供一個listIterator()方法,返回一個 ListIterator接口,和標準的Iterator接口相比,ListIterator多了一些add()之類的方法,允許添加,刪除,設定元素, 還能向前或向後遍歷。

實現List接口的常用類有LinkedList,ArrayList,Vector和Stack。

主要方法:

void add(int index,Object element)在指定位置上添加一個對象

boolean addAll(int index,Collection c)將集合c的元素添加到指定的位置

Object get(int index)返回List中指定位置的元素

int indexOf(Object o)返回第一個出現元素o的位置.

Object removeint(int index)刪除指定位置的元素

3. LinkedList類

LinkedList實現了List接口,採用雙向鏈表方式存儲數據,允許null元素。此外LinkedList提供額外的get,remove,insert方法在 LinkedList的首部或尾部。這些操作使LinkedList可被用作堆棧(stack),隊列(queue)或雙向隊列(deque)。

注意:LinkedList沒有同步方法。如果多個線程同時訪問一個List,則必須自己實現訪問同步。一種解決方法是在創建List時構造一個同步的List:

List list = Collections.synchronizedList(new LinkedList(...));

4. ArrayList類

ArrayList實現了可變大小的數組,採用數組方式存儲數據,它允許所有元素,包括null。

size,isEmpty,get,set方法運行時間爲常數。但是add方法開銷爲分攤的常數,添加n個元素需要O(n)的時間。其他的方法運行時間爲線性。

每個ArrayList實例都有一個容量(Capacity),即用於存儲元素的數組的大小。這個容量可隨着不斷添加新元素而自動增加,但是增長算法並沒有定義。當需要插入大量元素時,在插入前可以調用ensureCapacity方法來增加ArrayList的容量以提高插入效率。

和LinkedList一樣,ArrayList也是非同步的(unsynchronized)。

主要方法:

Boolean add(Object o)將指定元素添加到列表的末尾

Boolean add(int index,Object element)在列表中指定位置加入指定元素

Boolean addAll(Collection c)將指定集合添加到列表末尾

Boolean addAll(int index,Collection c)在列表中指定位置加入指定集合

Boolean clear()刪除列表中所有元素

Boolean clone()返回該列表實例的一個拷貝

Boolean contains(Object o)判斷列表中是否包含元素

Boolean ensureCapacity(int m)增加列表的容量,如果必須,該列表能夠容納m個元素

Object get(int index)返回列表中指定位置的元素

Int indexOf(Object elem)在列表中查找指定元素的下標

Int size()返回當前列表的元素個數

5. Vector類

Vector非常類似ArrayList,採用數組方式存儲數據,但是Vector是同步的。由Vector創建的Iterator,雖然和ArrayList創建的 Iterator是同一接口,但是,因爲Vector是同步的,當一個Iterator被創建而且正在被使用,另一個線程改變了Vector的狀態(例如,添加或刪除了一些元素),這時調用Iterator的方法時將拋出ConcurrentModificationException,因此必須捕獲該異常。

6. Stack 類

Stack繼承自Vector,實現一個後進先出的堆棧。

Stack提供5個額外的方法使得Vector得以被當作堆棧使用。基本的push和pop方法,還有peek方法得到棧頂的元素,empty方法測試堆棧是否爲空,search方法檢測一個元素在堆棧中的位置。

Stack剛創建後是空棧。

7. Set接口

Set是一種不包含重複的元素的Collection,即任意的兩個元素e1和e2都有e1.equals(e2)=false,Set最多有一個null元素。很明顯,Set的構造函數有一個約束條件,傳入的Collection參數不能包含重複的元素。

注意:必須小心操作可變對象(Mutable Object)。如果一個Set中的可變元素改變了自身狀態導致Object.equals(Object)=true將導致一些問題。

8. Map接口

Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個 value。

Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。

主要方法:

boolean equals(Object o)比較對象

boolean remove(Object o)刪除一個對象

put(Object key,Object value)添加key和value

9. Hashtable類

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

添加數據使用put(key, value),取出數據使用get(key),這兩個基本操作的時間開銷爲常數。

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

使用Hashtable的簡單示例如下,將1,2,3放到Hashtable中,他們的key分別是”one”,”two”,”three”:

Hashtable numbers = new Hashtable(); 
numbers.put(“one”, new Integer(1)); 
numbers.put(“two”, new Integer(2)); 
numbers.put(“three”, new Integer(3));

要取出一個數,比如2,用相應的key:

Integer n = (Integer)numbers.get(“two”); 
System.out.println(“two = ” + n);

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

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

10. HashMap類

HashMap和Hashtable類似,不同之處在於HashMap是非同步的,並且允許null,即null value和null key。但是將HashMap視爲Collection時(values()方法可返回Collection),其迭代子操作時間開銷和HashMap的容量成比例。因此,如果迭代操作的性能相當重要的話,不要將HashMap的初始化容量設得過高,或者load factor過低。

11. WeakHashMap類

WeakHashMap是一種改進的HashMap,它對key實行“弱引用”,如果一個key不再被外部所引用,那麼該key可以被GC回收。

二、集合類數據結構

1. List集合

1) ArrayList 順序存儲

ArrayList維護着一個對象數組。如果調用new ArrayList()後,它會默認初始一個size=10的數組。 每次add操作都要檢查數組容量,如果不夠,重新設置一個初始容量1.5倍大小的新數組,然後再把每個元素copy過去(使用System.arraycopy())。 在數組中間插入或刪除,都要移動後面的所有元素。

數據結構圖:

 image

2) LindedList 鏈式存儲

LinkedList的實現是一個雙向鏈表。每個節點除含有元素外,還包含向前,向後的指針。 新建一個LinkedList,生成一個頭節點(header,就是一個頭指針),它的元素爲null。

數據結構圖:

image

它自包含,next和previous指針都指向自己。 執行add(Object obj)方法後,會生成一個新節點:

數據結構圖:

image

Header節點的next指向鏈表的第一個節點,previous指向鏈表的最後一個節點,在這裏都是first。再增加一個對象,它的形狀像下面這樣:

數據結構圖:

image

現在是一個標準的雙向鏈表形狀。每個節點都有自己的next和previous指針。

增加節點,只會對鏈表的指針進行操作,速度快;LinkedList實現了Deque,所以它有雙向隊列的特徵,在鏈表兩端可增刪數據;使用index查找對象時,會以index和size/2比較,從前或從後向中間搜索;ListIterator可向前或向後進行迭代;

3) 比較ArrayList和LinkedList的數據結構,就可以得出:

1. ArrayList是實現了基於動態數組的數據結構,LinkedList基於雙向指針鏈表的數據結構;

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

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

4. LinkedList不支持高效的隨機元素訪問;

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

可以這樣說:

1. 當操作是在一列數據的後面添加數據而不是在前面或中間,並且需要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能;

2. 當你的操作是在一列數據的前面或中間添加或刪除數據,並且按照順序訪問其中的元素時,就應該使用LinkedList了;

4) 淺談數組鏈表與指針鏈表

數組鏈表訪問快,複雜度O(1),但是添加刪除慢,複雜度O(n); 
指針鏈表訪問慢,複雜度是O(n),但是添加刪除快,複雜度O(1);

至於選擇哪種數據結構,只不過一般有習慣而已,比如二叉樹,一般都是用指針實現,你想用數組實現也沒有任何問題.而且有的時候算法需要數組實現. 你需要了解一個數據結構特點,進行算法複雜度分析,就能夠針對你的應用程序選擇合適的方法.

2. Map集合

1) HashMap

HashMap的結構是一個散列桶,初始化時生成如下結構:

image

每個bucket包含一個Entry(map自定義的一種結構,包含一個往後的指針)的鏈表。 在put(key, value)後,它的結構如下:

未命名

將key的hashcode再次散列,然後用這個hash和length-1進行按位與操作,得到bucket的index,然後檢查當前bucket的鏈表,有沒有這個key,如果有替換value,沒有則跟在鏈表的最後。

允許key和value都可以是null。Index=0的bucket存key=null的value,也可以是其它hashcode爲0的項。

初始容量必須爲2的冪次(我的理解是,在生成index的時候有這樣的代碼:hash ^ (length - 1)),length – 1的二進制代碼爲全1,則容易進行hash的設計)。

如果兩個key散列後的index一樣的話,第一個key生成的Entry先存在桶中,第二個key生成的Entry會將第一個Entry設爲自己的next,串起來。(如圖中,先put(yy, “first”),會將這個Entry設爲bucket的第一項,後put(xx,”second”),則生成新Entry,它的next爲key爲yy的Entry,生成一個鏈表), 在put操作中,會比較threshold(capacity * load_factor,一個臨界值),如果size > threshold的話,生成一個當前bucket兩倍數量的buckets,然後把現有的數據重新散列到新bucket中。

對HashMap迭代時,返回數據的順序是:index從0到length-1,循環遍歷每個bucket,把不爲null的數據取出,每個bucket內的順序由鏈表的順序決定。而不是由插入數據決定。

2) LinkedHashMap

上面說過,Map的迭代不由插入順序決定。如果要保持這種順序呢?就要新增加一種結構來保持。

未命名

LinkedHashMap是HashMap的子類,增加一個雙向鏈表,用來存儲每個新加入的節點。在遍歷時,按鏈表的順序進行。其實差不多就是上面HashMap和LinkedList的和吧。

3. Set

1) HashSet

HashSet使用HashMap來保持元素。Key = 元素,value是一個公有的對象,對每個元素都一樣,在HashMap裏面key是惟一的,當然很適合於構造set集合。等同於用HashMap包裝了層,顯示Set自己的特性。

三、ArrayList、Vector與LinkedList分析

1. 同步性

Vector是同步的。這個類中的一些方法保證了Vector中的對象是線程安全的。而ArrayList則是非同步的,因此ArrayList中的對象並不是線程安全的。因爲同步的要求會影響執行的效率,所以如果你不需要線程安全的集合那麼使用ArrayList是一個很好的選擇,這樣可以避免由於同步帶來的不必要的性能開銷。

2. 數據增長

從內部實現機制來講ArrayList和Vector都是使用數組(Array)來控制集合中的對象。當你向這兩種類型中增加元素的時候,如果元素的數目超出了內部數組目前的長度它們都需要擴展內部數組的長度,Vector缺省情況下自動增長原來一倍的數組長度,ArrayList是原來的50%,所以最後你獲得的這個集合所佔的空間總是比你實際需要的要大。所以如果你要在集合中保存大量的數據,那麼使用Vector有一些優勢,因爲你可以通過設置集合的初始化大小來避免不必要的資源開銷。

3. 使用模式

在ArrayList和Vector中,從一個指定的位置(通過索引)查找數據或是在集合的末尾增加、移除一個元素所花費的時間是一樣的,這個時間我們用O(1)表示。但是,如果在集合的其他位置增加或移除元素那麼花費的時間會呈線形增長:O(n-i),其中n代表集合中元素的個數,i代表元素增加或移除元素的索引位置。爲什麼會這樣呢?以爲在進行上述操作的時候集合中第i和第i個元素之後的所有元素都要執行位移的操作。這一切意味着什麼呢?

這意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那麼使用Vector或ArrayList都可以。如果是其他操作,你最好選擇其他的集合操作類。比如,LinkedList集合類在增加或移除集合中任何位置的元素所花費的時間都是一樣的O(1),但它在索引一個元素的使用卻比較慢O(i),其中i是索引的位置.使用ArrayList也很容易,因爲你可以簡單的使用索引來代替創建iterator對象的操作。LinkedList也會爲每個插入的元素創建對象(會重新分配空間),所以你要明白它也會帶來額外的開銷。

ArrayList底層採用數組完成,而LinkedList則是採用一般的雙向鏈表(double-linked list)完成,其內每個對象除了數據本身外,還有兩個引用,分別指向前一個元素和後一個元素。如果我們經常在List的開始處增加元素,或者在List中進行插入和刪除操作,我們應該使用LinkedList,否則的話,使用ArrayList將更加快速。當執行搜索操作時,採用ArrayList 比較好。

最後,在《Practical Java》一書中Peter Haggar建議使用一個簡單的數組(Array)來代替Vector或ArrayList。尤其是對於執行效率要求高的程序更應如此。因爲使用數組(Array)避免了同步、額外的方法調用和不必要的重新分配空間的操作。

4. 集合類的區別

1. ArrayList: 元素單個,效率高,多用於查詢 ; 
2. Vector: 元素單個,線程安全,多用於查詢 ; 
3. LinkedList:元素單個,多用於插入和刪除 ; 
4. HashMap: 元素成對,元素可爲空 ; 
5. HashTable: 元素成對,線程安全,元素不可爲空 ;

四、Collections和Arrays幫助類

在 Java集合類框架裏有兩個類叫做Collections(注意,不是Collection!)和Arrays,這是JCF裏面功能強大的工具,但初學者往往會忽視。按JCF文檔的說法,這兩個類提供了封裝器實現(Wrapper Implementations)、數據結構算法和數組相關的應用。

想必大家不會忘記上面談到的“折半查找”、“排序”等經典算法吧,Collections類提供了豐富的靜態方法幫助我們輕鬆完成這些在數據結構課上煩人的工作:

binarySearch:折半查找。 
sort:排序,這裏是一種類似於快速排序的方法,效率仍然是O(n * log n),但卻是一種穩定的排序方法。 
reverse:將線性表進行逆序操作,這個可是從前數據結構的經典考題哦! 
rotate:以某個元素爲軸心將線性表“旋轉”。 
swap:交換一個線性表中兩個元素的位置。 
……

Collections還有一個重要功能就是“封裝器”(Wrapper),它提供了一些方法可以把一個集合轉換成一個特殊的集合:

unmodifiableXXX:轉換成只讀集合,這裏XXX代表六種基本集合接口:Collection、List、Map、Set、SortedMap和SortedSet。如果你對只讀集合進行插入刪除操作,將會拋出UnsupportedOperationException異常。 
synchronizedXXX:轉換成同步集合。 
singleton:創建一個僅有一個元素的集合,這裏singleton生成的是單元素Set,singletonList和singletonMap分別生成單元素的List和Map。 
空集:由Collections的靜態屬性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示。

五、總結

大多數情況下,從性能上來說ArrayList最好,但是當集合內的元素需要頻繁插入、刪除時LinkedList會有比較好的表現,但是它們三個性能都比不上數組,另外Vector是線程同步的。所以:

如果能用數組的時候(元素類型固定,數組長度固定),請儘量使用數組來代替List; 
如果沒有頻繁的刪除插入操作,又不用考慮多線程問題,優先選擇ArrayList; 
如果在多線程條件下使用,可以考慮Vector; 
如果需要頻繁地刪除插入,LinkedList就有了用武之地; 
如果你什麼都不知道,用ArrayList沒錯。

要特別注意對哈希表的操作,作爲key的對象要正確複寫equals和hashCode方法。

最後,編程時儘量返回接口而非實際的類型,如返回List而非ArrayList,這樣如果以後需要將ArrayList換成LinkedList時,客戶端代碼不用改變。這就是針對接口編程。

發佈了4 篇原創文章 · 獲贊 0 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章