JAVA常用集合框架用法詳解——提高篇
這篇文章是我對集合框架的昇華總結。文章中沒有提到各個集合子類的最基本的操作和方法。想要知道這部分的內容,可以檢視我的一篇基礎知識的部落格--Java集合框架總結基礎篇http://blog.csdn.net/lulei1217/article/details/45167433。
這幾天一直在看Java的集合框架。通過這幾天的學習使我對集合有了一個全新的認識,現在來說說吧。先上集合的家譜圖一張,來自Java程式設計思想書中的。
首先從容器的老祖先Collection介面說起。Collection介面有3個子介面依次是:List、Set 、Queue(這個不常用)。
上面是介紹了一些有關容器組成的一些內容。其中比較常用的是ArrayList、LinkedList、hashSet和hashMap。在定義這些容器物件的時候我們習慣使用向上轉型的做法
List list=new ArrayList();
這樣做的目的是限制了list物件的特有性。這樣我們在進行將list物件轉成LinkedList是沒有任何問題的。這樣的做法體現了面向物件的多型性。
--------------------------------------------*--------------------------------------------------
LinkedList link=new LinkedList(list);
上面的這種寫法是有問題的,因為沒有考慮的泛型的使用。主要原因是編譯器允許你向容器中新增錯誤的型別,比如你想向容器中新增的全是Apple類,但是一不小心放了一個Orange類進去了。這時候編譯是沒有任何的問題的,但是在執行過程中一旦想遍歷容器中的元素就會出現型別異常的錯誤。為此我們必須在宣告和定義的時候就應該說明我這個容器只能裝Apple類。我們可以修改程式碼如下:(加上泛型)
List<apple> list=new ArrayList<apple>();
如果還要考慮apple類會有子類那就需要這樣修改:
List<? extends apple> list=new ArrayList<? extends apple>();
所以建議大家在使用容器的時候應該加上泛型。使用泛型後,你會發現如果再次將orange類加入容器中在編譯期就會報錯。
在所有的容器子類中,這裡不得不說一下我們的容器一般只裝物件,不會在容器中裝基本資料型別(byte,int等是需要進行裝箱處理的)。這樣是不能通過編譯的。如果我們想列印容器中的物件,就必須遍歷這個容器類。如何遍歷這個容器類呢?
這裡Java API給我們提供了一個叫迭代器物件的機制。在Java中,迭代器Iterator物件實際上是一個介面,它有一個子介面叫ListIterator(只能在List中使用)。我們所有的容器類都會為我們提供了一個叫Iterator的迭代器物件,通過使用Iterator的一些方法來進行遍歷操作。比如:
List<apple> list=new ArrayList<apple>();
list.add(new apple());//這個方法是向容器中新增元素
list.add(new apple());
list.add(new apple());
list.add(new apple());
list.add(new apple());
//獲取容器的迭代器物件
Iterator<apple> it=list.iterator();
while(it.hasNext()){
apple a=it.next();
System.out.println(a);//輸出容器中的物件
it.remove();//表示將容器中的物件給刪除了
}
這裡我們注意:
a.使用迭代器物件來操作容器中的元素(如:remove)和直接操作容器本身是一樣的。
b.Iterator迭代器只有remove操作方法,如果想讓迭代器物件可以給容器add新增元素和set修改元素則必須使用ListIterator迭代器。這個迭代器和他的名字一樣只能操作List容器,當然這個迭代器支援容器物件的向後和向前的遍歷。
在java1.5之後,對於Collection容器官方是希望我們優先考慮使用Iterable迭代器,而Map容器則繼續使用Iterator迭代器。Iterable介面實際上是包含了一個返回Iterator介面的iterator()方法。
當然這裡我在提提foreach的語法。在介紹容器之前,我們主要是在陣列中使用foreach的。其實在Collection容器中我們也會使用到的。因為我們在foreach中可以直接的在容器中移動Iterable介面的。這就是我說的為什麼優先使用Iterable,因為只要實現這個介面我們就允許容器物件成為 "foreach" 語句的目標。 這樣就不需要獲取迭代器物件了。上一段程式碼:
import java.util.*;
public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how " +
"we know the Earth to be banana-shaped.").split(" ");
public Iterator<String> iterator() {
return new Iterator<String>() {//返回的是Iterator物件
private int index = 0;
public boolean hasNext() {
return index < words.length;
}
public String next() { return words[index++]; }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(String s : new IterableClass())
System.out.print(s + " ");
}
}
輸出結果:
And that is how we know the Earth to be banana-shaped.
這裡需要注意一個我們在定義某個方法的時候引數可能是個Iterable。比如:
static<T> viod test(Iterable<T> ib){
for(T t:ib){
System.out.println(t);
}
}
public static void main(String args[]){
test(Arrays.asList(1,2,3));//這樣寫是正確的
String[] strs={”123”,”sdfd”,”sdf”};
//test(strs);//不能通過編譯
test(Arrays.asList(strs));//這樣可以
}
注意:當引數是Iterable的時候,我們可以傳List容器類過去,因為List是可以自動轉換的。但是不能直接傳陣列進去,必須將陣列先轉化為List。還有一個不得不提,在我們new一個容器的時候,我們一般是不太確定容器中元素的個數的,但是使用由陣列轉化為的List容器實際上我們是知道容器中元素的個數的。
---------------------------------*-----------------------------------
下面將要詳細的介紹List、Set和Map的子類知識。首先就是這些容器的子類都是執行緒不同步的。都是執行緒不安全的。
Collection 介面派生的介面是 List 和 Set還有Queue。
Collection 介面提供的主要方法:
1. boolean add(Object o) 新增物件到集合;
2. boolean remove(Object o) 刪除指定的物件;
3. int size() 返回當前集合中元素的數量;
4. boolean contains(Object o) 查詢集合中是否有指定的物件;
5. boolean isEmpty() 判斷集合是否為空;
6. Iterator iterator() 返回一個迭代器;
7. boolean containsAll(Collection c) 查詢集合中是否有集合 C 中的元素;
8. boolean addAll(Collection c) 將集合 C 中所有的元素新增給該集合;
9. void clear() 刪除集合中所有元素;
10. void removeAll(Collection c) 從集合中刪除 C 集合中也有的元素;
11. void retainAll(Collection c) 從集合中刪除集合 C 中不包含的元素。
但是我們的子介面還是添加了許多的針對自己介面特性的一些方法。這裡所有的自己的特性指的是:
List承諾可以將元素維護在指定的序列中。有兩種主要的List派生類ArrayList和LinkedList。
ArrayList類他的底層資料結構是由陣列完成的,所以他的特點就是適合隨機訪問元素,直接類似於陣列的Arrays[i]的操作。但是對於刪除指定位置的元素和在指定的位置中插入元素就會比較麻煩的。因為會涉及到大量的陣列元素的移動和複製。
LinkedList類他的底層資料結構是由一個自定義的Node節點類來完成的。這個節點類有3個屬性:T date(用來儲存元素的)、Node next(指向下一個Node類節點的)、Node Preview(指向前一個Node類節點)。LinkedList就是由這些Node類組成的連結串列結構組成的。所以LinkedList類適合插入和刪除元素的操作,但是對於索引操作就比較的費事了。只能是由傳統的迭代器的方式依次來遍歷。屬於比較耗時的線性操作。
下面使用程式碼來檢視:
package com.wq520.rongqi;
import java.util.ArrayList;
import java.util.LinkedList;
public class TestArrayListAndLinkedList {
public static void main(String[] args){
ArrayList<Object> Alist = new ArrayList<Object>();
Object obj = new Object();
System.out.println("ArrayList的add方法耗時:(毫秒)");
long start = System.currentTimeMillis();
for(int i=0;i<5000000;i++){
Alist.add(obj);
}
long end = System.currentTimeMillis();
System.out.println(end-start);//檢視ArrayList新增元素的耗時時間
LinkedList<Object> Llist = new LinkedList<Object>();
Object obj1 = new Object();
System.out.println("LinkedList的add方法耗時:(毫秒)");
start = System.currentTimeMillis();
for(int i=0;i<5000000;i++){
Llist.add(obj1);
}
end = System.currentTimeMillis();
System.out.println(end-start);//檢視LinkedList新增元素的耗時時間
System.out.println("ArrayList的指定位置插入方法耗時:(毫秒)");
start = System.currentTimeMillis();
Object obj2 = new Object();
for(int i=0;i<1000;i++){
Alist.add(0,obj2);
}
end = System.currentTimeMillis();
System.out.println(end-start);
System.out.println("LinkedList的指定位置插入方法耗時:(毫秒)");
start = System.currentTimeMillis();
Object obj3 = new Object();
for(int i=0;i<1000;i++){
Llist.add(0,obj3);
}
end = System.currentTimeMillis();
System.out.println(end-start);
System.out.println("ArrayList的指定位置remove方法耗時:(毫秒)");
start = System.currentTimeMillis();
Alist.remove(0);
end = System.currentTimeMillis();
System.out.println(end-start);
System.out.println("LinkedList的指定位置remove方法耗時:(毫秒)");
start = System.currentTimeMillis();
Llist.remove(0);
end = System.currentTimeMillis();
System.out.println(end-start);
}
}
輸出結果:
ArrayList的add方法耗時:(毫秒)
216
LinkedList的add方法耗時:(毫秒)
687
ArrayList的指定位置插入方法耗時:(毫秒)
8803
LinkedList的指定位置插入方法耗時:(毫秒)
0
ArrayList的指定位置remove方法耗時:(毫秒)
8
LinkedList的指定位置remove方法耗時:(毫秒)
0
---------------------------------*----------------------------
這裡在來談談LinkedList類。在實際的應用中,我們會經常將LinkedList用來實現Stack(堆疊)、Queue(佇列)。主要是由於LinkedList的雙鏈表結構可以輕鬆的完成Stack的進棧和出棧的操作和佇列的出佇列和入佇列的操作。但是用歸用,我們這裡不得不提的是如果直接使用如下的方式是很危險的:
List<T> stack=new LinkedList<T>();
因為這樣做會使我們所謂的堆疊stack可以直接的使用了許多LinkedList類的特有的操作方法。一般也沒人用這種方法。正確的做法就是自己定義一個Stack類,然後內部使用LinkedList來完成所有的堆疊專有的操作。即所謂的封裝堆疊特有的一些操作方法。下面是定義的Stack類
public class Stack<T>{
private LinkedList<T> stack=new LinkedList<T>();
public void push(T t){
stack.addFirst(v);//壓棧操作
}
public T peek(){
return stack.getFirst();//返回到棧頂
}
public T pop(){
return stack.removeFirst();//出棧操作
}
public boolean empty(){
return stack.isEmpty();
}
public String toString(){
return stack.toString();
}
}
好了,這裡使用了通用泛型,引入了一些最基本的棧的操作方法。之後我們就可以將該類作為堆疊使用了。
--------------------------------*----------------------------------
接下來說說Set容器介面。該介面使用較多的的派生類是HashSet,TreeSet,和LinkedHashSet(HashSet的子類)。所有的Set介面的派生類都有一個特點就是類中所放的元素是不會重複的。也是說Set中的每一個元素都是唯一的。由於要保證元素的唯一性,所以加入Set的元素必須要定義自己的equals()方法以確保物件的唯一性。同時,Set容器中的元素是亂序的,不會維護元素的次序的。
談談Set的派生類。首先是:
HashSet:他是為了快速查詢而設計的Set,存入HashSet的元素(記住,是個物件啊)必須要定義hashCode()方法。
TreeSet:他是儲存次序的Set,底層的實現是樹結構。使用它可以從Set中提取有序的序列。但是元素必須要實現Comparable介面。該介面是用於提供元素的排序規則的。
LinkdHashSet: 它有HashSet的查詢速度,且內部使用連結串列維護元素的順序(插入順序)。於是在使用迭代器遍歷Set時,結果會按元素插入的次序顯示。元素也必須定義hashCode()方法。
----------------------------*----------------------------------
因為HashSet存入的是唯一的元素,所以我們必須能夠使用一種方法來保證元素的唯一性。這裡我們通常在元素類中覆寫hashCode()來計算雜湊值的方法。一般來說我們還會在元素類中覆寫equals()方法,主要是確保當前的元素物件的確是唯一的。這裡我們就會確保元素的唯一性,根據不同的雜湊值來判斷。當然我這樣說是有不妥的,後面在講解具體的雜湊值是怎麼實現的。
現在一旦有這個元素的雜湊值和Set容器中的任何一個元素的雜湊值是一樣的,容器就會拒絕新增該元素。當然HashSet類除了操作比較快,就是元素的無序性。這也是它快的主要原因所在。
但是我們想讓一個Set容器儲存元素即能夠是唯一的(這點是肯定的)又能夠是有序的。這時候使用HashSet就沒轍了。我們這時候只能使用TreeSet了。
預設的時候,當我們向TreeSet容器中新增元素的時候,容器會將我們的元素按照自然順序來排序。但是我們是可以自己修改這種排序規則的。這時我們必須讓新增的元素類實現一個叫做Comparable的介面。並且還要覆寫該介面的compareTo()方法,在該方法裡我們可以編寫自己的排序規則程式碼。所以一旦你定義了一個TreeSet容器,我建議你必須元素類實現Comparable介面,並且實現compareTo()方法。
-----------------------------*--------------------------------
接下來說說佇列。在Java SE中目前Queue的實現只有兩個就是LinkList和PriorityQueue。他們兩的差異不是他們的效能不一樣,而是他們的排序行為不一樣。
LinkedList是按照標準的火車站買票模型(不準插隊的模式哦)來進行出佇列和入佇列,所以說第一個排隊的肯定是第一個出來的。
PriorityQueue類是按照優先順序來將當前容器中的元素先進行個排序,然後在出佇列。比如說同樣是火車站買票的模型,可能有的人排在最前但是火車要等好久才到站,而有的人是排在後面但是火車即將到站了,所以必須進行購票上車。一般購票員也允許他們插隊,假如我們將當前排隊的人按照火車到站的順序來進行購票排隊。這樣不至於有些人還沒買票火車就走的現象出現。這個按照火車到站的順序就是一個排序規則。
我們的PriorityQueue容器就可以完成上面所說的改進的排隊方式。這裡容器的物件必須實現一個Comparable的介面,同時要覆寫compareTo()方法,這個方法就是寫我們自定義的優先出佇列的規則。這時容器中的元素就會按照這個規則出佇列。
在這裡不得不提的一個就是Java API中是沒有雙向連結串列的。如果想實現雙向連結串列,我們可以通過組合LinkedList類來實現一個Dequeue類。並且直接在LinkedList中暴露一些操作方法。這種操作和之前的Stack類是一樣的。
----------------------------------*----------------------------------
最後我們來討論容器的另一個家族就是Map介面。這個介面取代了Dictionary抽象類(該類有個子方法叫HashTable),成為了新的解決物件與物件間的對映關係的新方法。Map容器儲存的元素是一個個的鍵值對(Key-Value)。我們可以簡單的理解為夫妻關係哦。
在生活中有太多可以使用Map容器表示的例子。比如某位先生的月收入是5000。我們可以這樣表示。這裡也是優先使用泛型的,並且優先考慮使用HashMap。
Map<Person,Integer> map=new HashMap<Person,Integer>();
map.put(new Person(“張三”),5000);
當然Map是和陣列一樣的是很容易就能擴充套件到多維的。比如某個人養了多少個寵物。Map格式如下:Map<Person,List<Pet>>。List<Pet>容器中表示儲存的寵物。
我們甚至可以在Map中存放Map作為值:比如某個人養了多少寵物,都是什麼物種和名字分別叫什麼。這樣的Map格式如下:
Map<Person,Map<Animal,Names>>。
從上面的例子我們可以看出Map就是一個對映表。這裡使用陣列來模擬一下Map的操作:
public class AssociativeArray<K,V> {
private Object[][] pairs;
private int index;
public AssociativeArray(int length) {
pairs = new Object[length][2];
}
public void put(K key, V value) {
if(index >= pairs.length)
throw new ArrayIndexOutOfBoundsException();
pairs[index++] = new Object[]{ key, value };
}
@SuppressWarnings("unchecked")
public V get(K key) {
for(int i = 0; i < index; i++)
if(key.equals(pairs[i][0]))
return (V)pairs[i][1];
return null; // Did not find key
}
public String toString() {
StringBuilder result = new StringBuilder();
for(int i = 0; i < index; i++) {
result.append(pairs[i][0].toString());
result.append(" : ");
result.append(pairs[i][1].toString());
if(i < index - 1)
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
AssociativeArray<String,String> map =
new AssociativeArray<String,String>(6);
map.put("sky", "blue");
map.put("grass", "green");
map.put("ocean", "dancing");
map.put("tree", "tall");
map.put("earth", "brown");
map.put("sun", "warm");
try {
map.put("extra", "object"); // Past the end
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Too many objects!");
}
System.out.println(map);
System.out.println(map.get("ocean"));
}
}
輸出結果是:
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing
-------------------------------*----------------------------------
上面的例子只是一個用陣列實現Map功能的類。Java API中肯定是不會用這種方法的。首先就是效能的問題,上面使用的get()方法使用的是線性搜尋,這種方法執行的速度會比較慢。所以Map介面的實現類使用了雜湊值來取代對鍵的線性搜尋。Map的實現類有:HashMap,LinkedHashMap,TreeMap,WeakHashMap,ConcurrentHashMap和IdentityHashMap。
注意在使用Map容器的時候我們要特別注意鍵元素(Key)的要求。它應該和Set容器中的元素是一樣的。任何的鍵都必須具有一個equals方法,如果鍵被用在了HashMap中,那麼鍵元素類就必須有一個合適的hashCode方法。但是如果鍵被用在了TreeMap中,那麼鍵元素類還必須要實現Comparable介面。
--------------------------*-----------------------------------
說到HashMap(或者HashSet)就不得不提雜湊值和雜湊函式(就是hashCode()方法)。在HashMap中我們是為了提高操作速度而不得不使用雜湊的。他的執行模式如下。
1、首先HashMap會為我們提供一個指定大小的可以存放鍵元素的雜湊值的桶位陣列。(可以看成陣列)
2、我們需要讓鍵元素類有自己的hashCode()和equals()方法。這裡會使用一些特定的雜湊函式來進行計算的。
3、HashMap容器會通過鍵元素計算的雜湊值,將其放入指定索引的桶位陣列中。注意這裡不是隨機放的,雖然表面上看是很亂。實際上是根據不同的雜湊方法獲取雜湊值,然後將雜湊值對桶位陣列求餘。
4、接著會在當前索引的位置中建立了一個連結串列(內部完成的),然後將該鍵(Key)對應的值(Value)放入連結串列中。這個連結串列是儲存計算雜湊值一樣但不是同一個例項物件的元素的。
5、如果下次插入的鍵元素計算的雜湊值是一樣的,就會將該鍵元素對應的值放入連結串列中儲存。
注意:桶位陣列的容量在HashMap的含參構造器中是可以設定的。
API中的定義如下:
(int initialCapacity, float loadFactor)
//構造一個帶指定初始容量和載入因子(即上面的桶位陣列中空桶位數量佔的比例低於loadFactor就需要新的桶位了)的空 HashMap。
還有一個TreeMap介面,它的一些操作和TreeSet是一樣的,只是儲存的物件不是單個的物件而是鍵值對這種對映關係物件對。他是一種建立有序Map的方式。排序規則是通過實現Comparable介面來完成的(即需要覆寫compareTo()方法)。
----------------------------------*----------------------------------
Map介面使用最多的就是get和put方法了。put方法是向Map介面中新增鍵值對,而get是取出鍵值對。在Map中我們是如何取元素的呢?這裡肯定是少不了遍歷的操作的。Map介面實際上給我們提供了3中集合檢視來供我們遍歷取值的。
1、首先是使用keySet()方法來獲取key的Set集合物件,然後遍歷這個Set集合物件即可。這是遍歷獲取Key的元素的方法。
2、第二種是使用values()方法來直接返回value的集合。然後在遍歷這個value集合。這時遍歷獲取Value的元素的方法。
3、第三種方法使用的較多。是使用entrySet()方法來返回一個Map.Entry<K,V>的鍵值對介面的集合。然後呼叫Map.Entry<K,V>的getKey()和getValue()方法來獲取鍵和值的物件。既然說Map是儲存對映關係的,所以我們肯定是希望Key和Value能夠一起出現的遍歷中。所以使用entrySet()的較多。