史上最全的Java集合類解析
本文僅分析部分原理和集合類的特點,不分析原始碼,旨在對java的集合類有一個整體的認識,理解各個不同類的關聯和區別,讓大家在不同的環境下學會選擇不同的類來處理。
Java中的集合類包含的內容很多而且很重要,很多資料的儲存和處理(排序,去重,篩選等)都需要通過集合類來完成。
首先java中集合類主要有兩大分支:
(1)Collection (2)Map
先看它們的類圖:
(1)Collection
(2)Map
可以看到它們之間的關係紛繁複雜,如果不繫統的學習一下,還真是不知道有什麼區別,該怎麼選擇。由於HashSet的內部實現原理是使用了HashMap,所以我們的學習路線為先學習Map集合類,然後再來學習Collection集合類。
(1)HashMap和Hashtable ( 注意table是小寫的t,搞不懂為什麼要這樣,老是會寫錯。。。)
首先來看HashMap和HashTable,這兩兄弟經常被放到一起來比較,那麼它們有什麼不一樣呢?
a.HashMap不是執行緒安全的;HashTable是執行緒安全的,其執行緒安全是通過Sychronize實現。
b.由於上述原因,HashMap效率高於HashTable。
c.HashMap的鍵可以為null,HashTable不可以。
d.多執行緒環境下,通常也不是用HashTable,因為效率低。HashMap配合Collections工具類使用實現執行緒安全。同時還有ConcurrentHashMap可以選擇,該類的執行緒安全是通過Lock的方式實現的,所以效率高於Hashtable。
好,比較了他們的不一樣後,來講講它們的原理。
陣列,連結串列,雜湊表。各有優劣,順便提一下,陣列連續記憶體空間,查詢速度快,增刪慢;連結串列充分利用了記憶體,儲存空間是不連續的,首尾儲存上下一個節點的資訊,所以定址麻煩,查詢速度慢,但是增刪快;雜湊表呢,綜合了它們兩個的有點,一個雜湊表,由陣列和連結串列組成。假設一條連結串列有1000個節點,現在查詢最後一個節點,就得從第一個遍歷到最後一個;如果用雜湊表,將這條連結串列分為10組,用一個容量為10陣列來儲存這10組連結串列的頭結點(a[0] = 0 , a[1] = 100 , a[2] = 200 …)。這樣定址就快了。
HashMap實現原理就是上述原理了,當然其具體實現還有很多其他的東西。Hashtable同理,只不過做了同步處理。
Hash碰撞,不同的key根據hash演算法算出的值可能一樣,如果一樣就是所謂的碰撞。
優化措施:
(1) HashMap的擴容代價非常大,要生成一個新的桶陣列,然後要把所有元素都重新Hash落桶一次,幾乎等於重新執行了一次所有元素的put。所以如果我們對Map的大小有一個範圍的話,可以在構造時給定大小,一般大小設定為:(int) ((float) expectedSize / 0.75F + 1.0F)。
(2) key的設計儘量簡潔。
HashMap一些功能實現:
a.按值排序
HashMap按值排序通過Collections的sort方法,在實現排序之前,我們先看看HashMap的幾種遍歷方式:
//Collection And Map
public static void testCM(){
//Collection
Map<Integer , String> hs = new HashMap<Integer , String>();
int i = 0;
hs.put(199, "序號:"+201);
while(i<50){
hs.put(i, "序號:"+i);
i++;
}
hs.put(-1, "序號:"+200);
hs.put(200, "序號:"+200);
//遍歷方式一:for each遍歷HashMap的entryset,注意這種方式在定義的時候就必須寫成
//Map<Integer , String> hs,不能寫成Map hs;
for(Entry<Integer , String> entry : hs.entrySet()){
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
}
//遍歷方式二:使用EntrySet的Iterator
Iterator<Map.Entry<Integer , String>> iterator = hs.entrySet().iterator();
while(iterator.hasNext()){
Entry<Integer , String> entry = iterator.next();
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
};
//遍歷方式三:for each直接使用HashMap的keyset
for(Integer key : hs.keySet()){
System.out.println("key:"+key+" value:"+hs.get(key));
};
//遍歷方式四:使用keyset的Iterator
Iterator keyIterator = hs.keySet().iterator();
while(keyIterator.hasNext()){
Integer key = (Integer)keyIterator.next();
System.out.println("key:"+key+" value:"+hs.get(key));
}
}
(1)使用keyset的兩種方式都會遍歷兩次,所以效率沒有使用EntrySet高。
(2)HashMap輸出是無序的,這個無序不是說每次遍歷的結果順序不一樣,而是說與插入順序不一樣。
接下來我們看按值排序,註釋比較詳細就不贅述過程了。
//對HashMap排序
public static void sortHashMap(Map<Integer , String> hashmap){
System.out.println("排序後");
//第一步,用HashMap構造一個LinkedList
Set<Entry<Integer , String>> sets = hashmap.entrySet();
LinkedList<Entry<Integer , String>> linkedList = new LinkedList<Entry<Integer , String>>(sets);
//用Collections的sort方法排序
Collections.sort(linkedList , new Comparator<Entry<Integer , String>>(){
@Override
public int compare(Entry<Integer , String> o1, Entry<Integer , String> o2) {
// TODO Auto-generated method stub
/*String object1 = (String) o1.getValue();
String object2 = (String) o2.getValue();
return object1.compareTo(object2);*/
return o1.getValue().compareTo(o2.getValue());
}
});
//第三步,將排序後的list賦值給LinkedHashMap
Map<Integer , String> map = new LinkedHashMap();
for(Entry<Integer , String> entry : linkedList){
map.put(entry.getKey(), entry.getValue());
}
for(Entry<Integer , String> entry : map.entrySet()){
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
}
}
b.按鍵排序
HashMap按鍵排序要比按值排序方法容易實現,而且方法很多,下面一一介紹。
第一種:還是熟悉的配方還是熟悉的味道,用Collections的sort方法,只是更改一下比較規則。
第二種:TreeMap是按鍵排序的,預設升序,所以可以通過TreeMap來實現。
public static void sortHashMapByKey(Map hashmap){
System.out.println("按鍵排序後");
//第一步:先建立一個TreeMap例項,建構函式傳入一個Comparator物件。
TreeMap<Integer , String> treemap = new TreeMap<Integer , String>(new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2) {
// TODO Auto-generated method stub
return Integer.compare(o1, o2);
}
});
//第二步:將要排序的HashMap新增到我們構造的TreeMap中。
treemap.putAll(hashmap);
for(Entry<Integer , String> entry : treemap.entrySet()){
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
}
}
第三種:可以通過keyset取出所有的key,然後將key排序,再有序的將key-value鍵值對存到LinkedHashMap中,這個就不貼程式碼了,有興趣的可以自己去嘗試一下。
c.value去重
對於HashMap而言,它的key是不能重複的,但是它的value是可以重複的,有的時候我們要將重複的部分剔除掉。
方法一:將HashMap的key-value對調,然後賦值給一個新的HashMap,由於key的不可重複性,此時就將重複值去掉了。最後將新得到的HashMap的key-value再對調一次即可。
d.HashMap執行緒同步
第一種:
Map<Integer , String> hs = new HashMap<Integer , String>();
hs = Collections.synchronizedMap(hs);
第二種:
ConcurrentHashMap<Integer , String> hs = new ConcurrentHashMap<Integer , String>();
(2)IdentifyHashMap
IdentityHashMap與HashMap基本相似,只是當兩個key嚴格相等時,即key1==key2時,它才認為兩個key是相等的 。IdentityHashMap也允許使用null,但不保證鍵值對之間的順序。
(3)WeakHashMap
WeakHashMap與HashMap的用法基本相同,區別在於:後者的key保留物件的強引用,即只要HashMap物件不被銷燬,其物件所有key所引用的物件不會被垃圾回收,HashMap也不會自動刪除這些key所對應的鍵值對物件。但WeakHashMap的key所引用的物件沒有被其他強引用變數所引用,則這些key所引用的物件可能被回收。WeakHashMap中的每個key物件儲存了實際物件的弱引用,當回收了該key所對應的實際物件後,WeakHashMap會自動刪除該key所對應的鍵值對。
接下來是Collection介面及其子類:
(4)ArrayList , LinkedList , Vector
(1)首先,說說它們的關係和區別。ArrayList和Vector本質都是用陣列實現的,而LinkList是用雙鏈表實現的;所以,Arraylist和Vector在查詢效率上比較高,增刪效率比較低;LinkedList則正好相反。ArrayList是執行緒不安全的,Vector是執行緒安全的,效率肯定沒有ArrayList高了。實際中一般也不怎麼用Vector,可以自己做執行緒同步,也可以用Collections配合ArrayList實現執行緒同步。
(2)Tips
前面多次提到擴容的代價很高,所以如果能確定容量的大致範圍就可以在建立例項的時候指定,注意,這個僅限於ArrayList和Vector喲:
ArrayList arrayList = new ArrayList(100);
arrayList.ensureCapacity(200);
Vector vector = new Vector(100);
vector.ensureCapacity(200);
(3)其他功能實現
a.排序
List的排序的話就是使用Collections的sort方法,構造Comparator或者讓List中的物件實現Comparaable都可以,這裡就不貼程式碼了。
b.去重
第一種:用Iterator遍歷,遍歷出來的放到一個臨時List中,放之前用contains判斷一下。
第二種:利用set的不可重複性,只需三步走。
//第一步:用HashSet的特性去重
HashSet tempSet = new HashSet(arrayList);
//第二步:將arrayList清除
tempSet.clear();
//第三步:將去重後的重新賦給List
arrayList.addAll(tempSet);
(5)Stack
Stack呢,是繼承自Vector的,所以用法啊,執行緒安全什麼的跟Vector都差不多,只是有幾個地方需要注意:
第一:add()和push(),stack是將最後一個element作為棧頂的,所以這兩個方法對stack而言是沒什麼區別的,但是,它們的返回值不一樣,add()返回boolean,就是新增成功了沒有;push()返回的是你新增的元素。為了可讀性以及將它跟棧有一丟丟聯絡,推薦使用push。
第二:peek()和pop(),這兩個方法都能得到棧頂元素,區別是peek()只是讀取,對原棧沒有什麼影響;pop(),從字面上就能理解,出棧,所以原棧的棧頂元素就沒了。
(6)HashSet和TreeSet
Set集合類的特點就是可以去重,它們的內部實現都是基於Map的,用的是Map的key,所以知道為什麼可以去重複了吧。
既然要去重,那麼久需要比較,既然要比較,那麼久需要了解怎麼比較的,不然它將1等於2了,你怎麼辦?
比較是基於hascode()方法和equals()方法的,所以必要情況下需要重新這兩個方法。