1. 程式人生 > >史上最全的Java集合類解析

史上最全的Java集合類解析

本文僅分析部分原理和集合類的特點,不分析原始碼,旨在對java的集合類有一個整體的認識,理解各個不同類的關聯和區別,讓大家在不同的環境下學會選擇不同的類來處理。

Java中的集合類包含的內容很多而且很重要,很多資料的儲存和處理(排序,去重,篩選等)都需要通過集合類來完成。

首先java中集合類主要有兩大分支:

(1)Collection (2)Map

先看它們的類圖:

(1)Collection

Collection

(2)Map
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()方法的,所以必要情況下需要重新這兩個方法。

好了,到了總結的時候了,其實你會發現集合類雖然看起來多,但是都是很有規律的。ArrayList,LinkedList一個無序,一個有序;HashSet,TreeSet一個無序,一個有序;HashMap,LinkedHasmMap,一個無序,一個有序;Vector和HashTable,Stack是執行緒安全的,但是效率低;執行緒不安全的類都可以配合Collections得到執行緒安全的類。