本文主要關注Java編程中涉及到的各種集合類,以及它們的使用場景?
1. Java集合類基本概念
在編程中,常常需要集中存放多個數(shù)據(jù)。從傳統(tǒng)意義上講,數(shù)組是我們的一個很好的選擇,前提是我們事先已經(jīng)明確知道我們將要保存的對象的數(shù)量。一旦在數(shù)組初始化時指定了這個數(shù)組長度,這個數(shù)組長度就是不可變的,如果我們需要保存一個可以動態(tài)增長的數(shù)據(jù)(在編譯時無法確定具體的數(shù)量),java的集合類就是一個很好的設計方案了。
集合類主要負責保存、盛裝其他數(shù)據(jù),因此集合類也被稱為容器類。所以的集合類都位于java.util包下,后來為了處理多線程環(huán)境下的并發(fā)安全問題,java5還在java.util.concurrent包下提供了一些多線程支持的集合類。
在學習Java中的集合類的API、編程原理的時候,我們一定要明白,"集合"是一個很古老的數(shù)學概念,它遠遠早于Java的出現(xiàn)。從數(shù)學概念的角度來理解集合能幫助我們更好的理解編程中什么時候該使用什么類型的集合類。
Java容器類類庫的用途是"保存對象",并將其劃分為兩個不同的概念:
1)?Collection 一組"對立"的元素,通常這些元素都服從某種規(guī)則 ? 1.1)?List必須保持元素特定的順序 ? 1.2)?Set不能有重復元素 ? 1.3)?Queue保持一個隊列(先進先出)的順序 2)?Map 一組成對的"鍵值對"對象
Collection和Map的區(qū)別在于容器中每個位置保存的元素個數(shù):
1)?Collection?每個位置只能保存一個元素(對象) 2)?Map保存的是"鍵值對",就像一個小型數(shù)據(jù)庫。我們可以通過"鍵"找到該鍵對應的"值"
2. Java集合類架構層次關系
1.?Interface?Iterable 迭代器接口,這是Collection類的父接口。實現(xiàn)這個Iterable接口的對象允許使用foreach進行遍歷,也就是說,所有的Collection集合對象都具有"foreach可遍歷性"。這個Iterable接口只 有一個方法:?iterator()。它返回一個代表當前集合對象的泛型
3. Java集合類的應用場景代碼
學習了集合類的基本架構框架之后,我們接著來學習它們各自的應用場景、以及細節(jié)處的注意事項
0x1: Set
HashSet
import?java.util.*;? //類A的equals方法總是返回true,但沒有重寫其hashCode()方法。不能保證當前對象是HashSet中的唯一對象 class?A { ????public?boolean?equals(Object?obj) ????{ ????????return?true; ????} } //類B的hashCode()方法總是返回1,但沒有重寫其equals()方法。不能保證當前對象是HashSet中的唯一對象 class?B { ????public?int?hashCode() ????{ ????????return?1; ????} } //類C的hashCode()方法總是返回2,且有重寫其equals()方法 class?C { ????public?int?hashCode() ????{ ????????return?2; ????} ????public?boolean?equals(Object?obj) ????{ ????????return?true; ????} } public?class?HashSetTest { ????public?static?void?main(String[]?args)? ????{ ????????HashSet?books?=?new?HashSet(); ????????//分別向books集合中添加兩個A對象,兩個B對象,兩個C對象 ????????books.add(new?A()); ????????books.add(new?A()); ????????books.add(new?B()); ????????books.add(new?B()); ????????books.add(new?C()); ????????books.add(new?C()); ????????System.out.println(books); ????} }
result:
[B@1,?B@1,?C@2,?A@3bc257,?A@785d65]
可以看到,如果兩個對象通過equals()方法比較返回true,但這兩個對象的hashCode()方法返回不同的hashCode值時,這將導致HashSet會把這兩個對象保存在Hash表的不同位置,從而使對象可以添加成功,這就與Set集合的規(guī)則有些出入了。所以,我們要明確的是: equals()決定是否可以加入HashSet、而hashCode()決定存放的位置,它們兩者必須同時滿足才能允許一個新元素加入HashSet
但是要注意的是: 如果兩個對象的hashCode相同,但是它們的equlas返回值不同,HashSet會在這個位置用鏈式結構來保存多個對象。而HashSet訪問集合元素時也是根據(jù)元素的HashCode值來快速定位的,這種鏈式結構會導致性能下降。
所以如果需要把某個類的對象保存到HashSet集合中,我們在重寫這個類的equlas()方法和hashCode()方法時,應該盡量保證兩個對象通過equals()方法比較返回true時,它們的hashCode()方法返回值也相等
LinkedHashSet
import?java.util.*;? public?class?LinkedHashSetTest { ????public?static?void?main(String[]?args)? ????{ ????????LinkedHashSet?books?=?new?LinkedHashSet(); ????????books.add("Java"); ????????books.add("LittleHann"); ????????System.out.println(books); ????????//刪除?Java ??????books.remove("Java"); ????????//重新添加?Java ????????books.add("Java"); ????????System.out.println(books); ????} }
元素的順序總是與添加順序一致,同時要明白的是,LinkedHashSetTest是HashSet的子類,因此它不允許集合元素重復
TreeSet
import?java.util.*; public?class?TreeSetTest { ????public?static?void?main(String[]?args)? ????{ ????????TreeSet?nums?=?new?TreeSet(); ????????//向TreeSet中添加四個Integer對象 ????????nums.add(5); ????????nums.add(2); ????????nums.add(10); ????????nums.add(-9); ????????//輸出集合元素,看到集合元素已經(jīng)處于排序狀態(tài) ????????System.out.println(nums); ????????//輸出集合里的第一個元素 ????????System.out.println(nums.first()); ????????//輸出集合里的最后一個元素 ????????System.out.println(nums.last()); ????????//返回小于4的子集,不包含4 ????????System.out.println(nums.headSet(4)); ????????//返回大于5的子集,如果Set中包含5,子集中還包含5 ????????System.out.println(nums.tailSet(5)); ????????//返回大于等于-3,小于4的子集。 ????????System.out.println(nums.subSet(-3?,?4)); ????} }
與HashSet集合采用hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數(shù)據(jù)結構來存儲集合元素。TreeSet支持兩種排序方式: 自然排序、定制排序
1. 自然排序:
TreeSet會調(diào)用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然后將集合元素按升序排序,即自然排序。如果試圖把一個對象添加到TreeSet時,則該對象的類必須實現(xiàn)Comparable接口,否則程序會拋出異常。
當把一個對象加入TreeSet集合中時,TreeSet會調(diào)用該對象的compareTo(Object obj)方法與容器中的其他對象比較大小,然后根據(jù)紅黑樹結構找到它的存儲位置。如果兩個對象通過compareTo(Object obj)方法比較相等,新對象將無法添加到TreeSet集合中(牢記Set是不允許重復的概念)。
注意: 當需要把一個對象放入TreeSet中,重寫該對象對應類的equals()方法時,應該保證該方法與compareTo(Object obj)方法有一致的結果,即如果兩個對象通過equals()方法比較返回true時,這兩個對象通過compareTo(Object obj)方法比較結果應該也為0(即相等)
看到這里,我們應該明白:
1)?對與Set來說,它定義了equals()為唯一性判斷的標準,而對于到了具體的實現(xiàn),HashSet、TreeSet來說,它們又會有自己特有的唯一性判斷標準,只有同時滿足了才能判定為唯一性 2)?我們在操作這些集合類的時候,對和唯一性判斷有關的函數(shù)重寫要重點關注
?2. 定制排序
TreeSet的自然排序是根據(jù)集合元素的大小,TreeSet將它們以升序排序。如果我們需要實現(xiàn)定制排序,則可以通過Comparator接口的幫助(類似PHP中的array_map回調(diào)處理函數(shù)的思想)。該接口里包含一個int compare(T o1, T o2)方法,該方法用于比較大小
import?java.util.*; class?M { ????int?age; ????public?M(int?age) ????{ ????????this.age?=?age; ????} ????public?String?toString() ????{ ????????return?"M[age:"?+?age?+?"]"; ????} } public?class?TreeSetTest4 { ????public?static?void?main(String[]?args)? ????{ ????????TreeSet?ts?=?new?TreeSet(new?Comparator() ????????{ ????????????//根據(jù)M對象的age屬性來決定大小 ????????????public?int?compare(Object?o1,?Object?o2) ????????????{ ????????????????M?m1?=?(M)o1; ????????????????M?m2?=?(M)o2; ????????????????return?m1.age?>?m2.age???-1 ????????????????????:?m1.age?<?m2.age???1?:?0; ????????????} ????????});???? ????????ts.add(new?M(5)); ????????ts.add(new?M(-3)); ????????ts.add(new?M(9)); ????????System.out.println(ts); ????} }
看到這里,我們需要梳理一下關于排序的概念
1)?equals、compareTo決定的是怎么比的問題,即用什么field進行大小比較 2)?自然排序、定制排序、Comparator決定的是誰大的問題,即按什么順序(升序、降序)進行排序 它們的關注點是不同的,一定要注意區(qū)分
EnumSet
import?java.util.*; enum?Season { ????SPRING,SUMMER,FALL,WINTER } public?class?EnumSetTest { ????public?static?void?main(String[]?args)? ????{ ????????//創(chuàng)建一個EnumSet集合,集合元素就是Season枚舉類的全部枚舉值 ????????EnumSet?es1?=?EnumSet.allOf(Season.class); ????????//輸出[SPRING,SUMMER,FALL,WINTER] ????????System.out.println(es1); ????????//創(chuàng)建一個EnumSet空集合,指定其集合元素是Season類的枚舉值。 ????????EnumSet?es2?=?EnumSet.noneOf(Season.class);? ????????//輸出[] ????????System.out.println(es2);? ????????//手動添加兩個元素 ????????es2.add(Season.WINTER); ????????es2.add(Season.SPRING); ????????//輸出[SPRING,WINTER] ????????System.out.println(es2); ????????//以指定枚舉值創(chuàng)建EnumSet集合 ????????EnumSet?es3?=?EnumSet.of(Season.SUMMER?,?Season.WINTER);? ????????//輸出[SUMMER,WINTER] ????????System.out.println(es3); ????????EnumSet?es4?=?EnumSet.range(Season.SUMMER?,?Season.WINTER);? ????????//輸出[SUMMER,FALL,WINTER] ????????System.out.println(es4); ????????//新創(chuàng)建的EnumSet集合的元素和es4集合的元素有相同類型, ????????//es5的集合元素?+?es4集合元素?=?Season枚舉類的全部枚舉值 ????????EnumSet?es5?=?EnumSet.complementOf(es4);? ????????//輸出[SPRING] ????????System.out.println(es5); ????} }
以上就是Set集合類的編程應用場景。那么應該怎樣選擇何時使用這些集合類呢?
1)?HashSet的性能總是比TreeSet好(特別是最常用的添加、查詢元素等操作),因為TreeSet需要額外的紅黑樹算法來維護集合元素的次序。只有當需要一個保持排序的Set時,才應該使用TreeSet,否則都應該使用HashSet 2)?對于普通的插入、刪除操作,LinkedHashSet比HashSet要略慢一點,這是由維護鏈表所帶來的開銷造成的。不過,因為有了鏈表的存在,遍歷LinkedHashSet會更快 3)?EnumSet是所有Set實現(xiàn)類中性能最好的,但它只能保存同一個枚舉類的枚舉值作為集合元素 4)?HashSet、TreeSet、EnumSet都是"線程不安全"的,通??梢酝ㄟ^Collections工具類的synchronizedSortedSet方法來"包裝"該Set集合。 SortedSet?s?=?Collections.synchronizedSortedSet(new?TreeSet(...));
?
0x2: List
ArrayList
如果一開始就知道ArrayList集合需要保存多少元素,則可以在創(chuàng)建它們時就指定initialCapacity大小,這樣可以減少重新分配的次數(shù),提供性能,ArrayList還提供了如下方法來重新分配Object[]數(shù)組
1)?ensureCapacity(int?minCapacity):?將ArrayList集合的Object[]數(shù)組長度增加minCapacity 2)?trimToSize():?調(diào)整ArrayList集合的Object[]數(shù)組長度為當前元素的個數(shù)。程序可以通過此方法來減少ArrayList集合對象占用的內(nèi)存空間
import?java.util.*; public?class?ListTest { ????public?static?void?main(String[]?args)? ????{ ????????List?books?=?new?ArrayList(); ????????//向books集合中添加三個元素 ????????books.add(new?String("輕量級Java?EE企業(yè)應用實戰(zhàn)")); ????????books.add(new?String("瘋狂Java講義")); ????????books.add(new?String("瘋狂Android講義")); ????????System.out.println(books); ????????//將新字符串對象插入在第二個位置 ????????books.add(1?,?new?String("瘋狂Ajax講義")); ????????for?(int?i?=?0?;?i?<?books.size()?;?i++?) ????????{ ????????????System.out.println(books.get(i)); ????????} ????????//刪除第三個元素 ????????books.remove(2); ????????System.out.println(books); ????????//判斷指定元素在List集合中位置:輸出1,表明位于第二位 ????????System.out.println(books.indexOf(new?String("瘋狂Ajax講義")));??//① ????????//將第二個元素替換成新的字符串對象 ????????books.set(1,?new?String("LittleHann")); ????????System.out.println(books); ????????//將books集合的第二個元素(包括) ????????//到第三個元素(不包括)截取成子集合 ????????System.out.println(books.subList(1?,?2)); ????}
Stack
注意Stack的后進先出的特點
import?java.util.*; public?class?VectorTest { ????public?static?void?main(String[]?args)? ????{ ????????Stack?v?=?new?Stack(); ????????//依次將三個元素push入"棧" ????????v.push("瘋狂Java講義"); ????????v.push("輕量級Java?EE企業(yè)應用實戰(zhàn)"); ????????v.push("瘋狂Android講義"); ????????//輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)?,?瘋狂Android講義] ????????System.out.println(v); ????????//訪問第一個元素,但并不將其pop出"棧",輸出:瘋狂Android講義 ????????System.out.println(v.peek()); ????????//依然輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)?,?瘋狂Android講義] ????????System.out.println(v); ????????//pop出第一個元素,輸出:瘋狂Android講義 ????????System.out.println(v.pop()); ????????//輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)] ????????System.out.println(v); ????} }
LinkedList
import?java.util.*; public?class?LinkedListTest { ????public?static?void?main(String[]?args)? ????{ ????????LinkedList?books?=?new?LinkedList(); ????????//將字符串元素加入隊列的尾部(雙端隊列) ????????books.offer("瘋狂Java講義"); ????????//將一個字符串元素加入棧的頂部(雙端隊列) ????????books.push("輕量級Java?EE企業(yè)應用實戰(zhàn)"); ????????//將字符串元素添加到隊列的頭(相當于棧的頂部) ????????books.offerFirst("瘋狂Android講義"); ????????for?(int?i?=?0;?i?<?books.size()?;?i++?) ????????{ ????????????System.out.println(books.get(i)); ????????} ????????//訪問、并不刪除棧頂?shù)脑?????????System.out.println(books.peekFirst()); ????????//訪問、并不刪除隊列的最后一個元素 ????????System.out.println(books.peekLast()); ????????//將棧頂?shù)脑貜棾?棧" ????????System.out.println(books.pop()); ????????//下面輸出將看到隊列中第一個元素被刪除 ????????System.out.println(books); ????????//訪問、并刪除隊列的最后一個元素 ????????System.out.println(books.pollLast()); ????????//下面輸出將看到隊列中只剩下中間一個元素: ????????//輕量級Java?EE企業(yè)應用實戰(zhàn) ????????System.out.println(books); ????} }
從代碼中我們可以看到,LinkedList同時表現(xiàn)出了雙端隊列、棧的用法。功能非常強大
?
0x3:?Queue
PriorityQueue
import?java.util.*; public?class?PriorityQueueTest { ????public?static?void?main(String[]?args)? ????{ ????????PriorityQueue?pq?=?new?PriorityQueue(); ????????//下面代碼依次向pq中加入四個元素 ????????pq.offer(6); ????????pq.offer(-3); ????????pq.offer(9); ????????pq.offer(0); ????????//輸出pq隊列,并不是按元素的加入順序排列, ????????//而是按元素的大小順序排列,輸出[-3,?0,?9,?6] ????????System.out.println(pq); ????????//訪問隊列第一個元素,其實就是隊列中最小的元素:-3 ????????System.out.println(pq.poll()); ????} }
PriorityQueue不允許插入null元素,它還需要對隊列元素進行排序,PriorityQueue的元素有兩種排序方式
1)?自然排序: 采用自然順序的PriorityQueue集合中的元素對象都必須實現(xiàn)了Comparable接口,而且應該是同一個類的多個實例,否則可能導致ClassCastException異常 2)?定制排序 創(chuàng)建PriorityQueue隊列時,傳入一個Comparator對象,該對象負責對隊列中的所有元素進行排序 關于自然排序、定制排序的原理和之前說的TreeSet類似
?ArrayDeque
import?java.util.*; public?class?ArrayDequeTest { ????public?static?void?main(String[]?args)? ????{ ????????ArrayDeque?stack?=?new?ArrayDeque(); ????????//依次將三個元素push入"棧" ????????stack.push("瘋狂Java講義"); ????????stack.push("輕量級Java?EE企業(yè)應用實戰(zhàn)"); ????????stack.push("瘋狂Android講義"); ????????//輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)?,?瘋狂Android講義] ????????System.out.println(stack); ????????//訪問第一個元素,但并不將其pop出"棧",輸出:瘋狂Android講義 ????????System.out.println(stack.peek()); ????????//依然輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)?,?瘋狂Android講義] ????????System.out.println(stack); ????????//pop出第一個元素,輸出:瘋狂Android講義 ????????System.out.println(stack.pop()); ????????//輸出:[瘋狂Java講義,?輕量級Java?EE企業(yè)應用實戰(zhàn)] ????????System.out.println(stack); ????} }
以上就是List集合類的編程應用場景。我們來梳理一下思路
1.?java提供的List就是一個"線性表接口",ArrayList(基于數(shù)組的線性表)、LinkedList(基于鏈的線性表)是線性表的兩種典型實現(xiàn) 2.?Queue代表了隊列,Deque代表了雙端隊列(既可以作為隊列使用、也可以作為棧使用) 3.?因為數(shù)組以一塊連續(xù)內(nèi)存來保存所有的數(shù)組元素,所以數(shù)組在隨機訪問時性能最好。所以的內(nèi)部以數(shù)組作為底層實現(xiàn)的集合在隨機訪問時性能最好。 4.?內(nèi)部以鏈表作為底層實現(xiàn)的集合在執(zhí)行插入、刪除操作時有很好的性能 5.?進行迭代操作時,以鏈表作為底層實現(xiàn)的集合比以數(shù)組作為底層實現(xiàn)的集合性能好
我們之前說過,Collection接口繼承了Iterable接口,也就是說,我們以上學習到的所有的Collection集合類都具有"可遍歷性"
Iterable接口也是java集合框架的成員,它隱藏了各種Collection實現(xiàn)類的底層細節(jié),向應用程序提供了遍歷Collection集合元素的統(tǒng)一編程接口:
1)?boolean?hasNext():?是否還有下一個未遍歷過的元素 2)?Object?next():?返回集合里的下一個元素 3)?void?remove():?刪除集合里上一次next方法返回的元素
iterator實現(xiàn)遍歷:
import?java.util.*; public?class?IteratorTest { ????public?static?void?main(String[]?args)? ????{ ????????//創(chuàng)建一個集合 ????????Collection?books?=?new?HashSet(); ????????books.add("輕量級Java?EE企業(yè)應用實戰(zhàn)"); ????????books.add("瘋狂Java講義"); ????????books.add("瘋狂Android講義"); ????????//獲取books集合對應的迭代器 ????????Iterator?it?=?books.iterator(); ????????while(it.hasNext()) ????????{ ????????????//it.next()方法返回的數(shù)據(jù)類型是Object類型, ????????????//需要強制類型轉換 ????????????String?book?=?(String)it.next(); ????????????System.out.println(book); ????????????if?(book.equals("瘋狂Java講義")) ????????????{ ????????????????//從集合中刪除上一次next方法返回的元素 ????????????????it.remove(); ????????????} ????????????//對book變量賦值,不會改變集合元素本身 ????????????book?=?"測試字符串";???? ????????} ????????System.out.println(books); ????} }
<p style="margin:10px auto;padding-top