Java SE基礎鞏固(四):集合類

1 集合概述

Java中有不少集合類,例如ArrayList,LinkedList,HashMap,TreeMap等。集合的功能就是容納多個對象,它們就像容器同樣(實際上,直接稱爲容器也沒有毛病,C++就是這樣稱呼的),當須要的時候,能夠從裏面拿出來,很是方便。在Java5提供了泛型機制以後,使容器有了在編譯期作類型檢查的能力,使用起來更加安全、方便。java

Java中的集合類主要是有兩個接口派生出來的:Collection和Map,以下所示:node

i3JEbd.png

i3JZVA.png

Collection又主要有Set,Queue,List三大接口,在此基礎上,又有多個實現類。Map接口下一樣有總多的實現類,例如HashMap,EnumMap,HashTable等。接下來我將會挑選幾個經常使用的集合類來具體討論討論。算法

2 ArrayList和LinkedList

List集合能夠說是最經常使用的集合了,比HashMap還經常使用,通常咱們寫代碼的時候一旦遇到須要存儲多個元素的狀況,就優先想到使用List集合,至於使用的是ArrayList實現類仍是LinkedList實現類,那就具體狀況具體分析了。數組

2.1 ArrayList

ArrayList實現了List接口,繼承了AbstractList抽象類,AbstractList抽象類實現了絕大部分List接口定義的抽象方法,因此咱們在ArrayList源碼中看不到大部分List接口中定義的抽象方法的實現。ArrayList的內部使用數組來存儲對象,這也是ArrayList這個名字的由來,其各類操做,例如get,add等都是基於數組操做的,下面是add方法的源碼:安全

public void add(int index, E element) {
    //先檢查index是否在一個合理的範圍內
    rangeCheckForAdd(index);
	
    //保證數組的容量足夠加入新的元素,發現不足夠的話會進行擴容操做
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //進行一次數組拷貝,這裏的elementData就是保存對象的Object數組
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //往數組中加入元素
    elementData[index] = element;
    //修改size大小
    size++;
}
複製代碼

解釋在註釋中給出了,get方法也很是簡單,就不浪費時間了。數據結構

2.2 LinkedList

LinkedList繼承了AbstractSequentialList類,AbstractSequentialList類又繼承了AbstractList類,同時LinkedList也固然有實現List接口的,並且還實現了Deque接口,這就比較有意思了,說明LinkedList不只僅是List,仍是一個Queue。下圖表示其繼承體系:併發

i3JrqJ.png

LinkedList的基於鏈表實現的List,這是和ArrayList最大的區別。LinkedList有一個Node內部類,用來表示節點,以下所示:app

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
複製代碼

該類有next指針和prev指針,可見是一個雙向鏈表。接下來看看LinkedList的add操做:dom

public void add(int index, E element) {
    //檢查index
    checkPositionIndex(index);
	
    //若是index和size相等,說明已經到最後了,直接在last節點後插入節點接口
    if (index == size)
        linkLast(element);
    else //不然就在index位置插入節點
        linkBefore(element, node(index));
}
複製代碼

在linkLast()和linkBefore()方法裏會涉及到鏈表的操做,其中LinkLast()的實現比較簡單,linkBefore()稍微複雜一些,但只要學過數據結構的朋友,看這些源碼應該沒什麼問題,在此不貼出源碼了,比較本文定位不是源碼解析。函數

2.3 ArrayList和LinkedList的區別

在前面的介紹中其實有說到過,在這裏總結一下:

  1. ArayyList是基於數組實現的,LinkedList是基於鏈表實現的,由於實現不一樣,他們的效率之間確定是有差異的,ArrayList的隨機訪問效率較高,但插入操做會涉及到數組拷貝,因此效率插入效率不高。LinkedList的插入效率可高可低,若是是在尾部插入,由於有一個last節點,因此尾部插入的速度很是快,但在其餘位置的插入效率並不高,對於隨機訪問來講,由於須要從頭開始遍歷節點,因此隨機訪問的效率並不高。
  2. 他們的繼承體系稍微有些區別,LinkedList還實現了Deque接口,這是比較有特色的。

3 SynchronizedList和Vector

之因此把他們倆發在一塊兒是由於它們是線程安全的列表集合。SynchronizedList是Collections工具類裏的一個內部靜態類,實現了List接口,繼承了SynchronizedCollection類,Vector是JDK早期的一個同步的List,和ArrayList的繼承體系徹底同樣,並且也是基於數組實現的,只是他的各類方法都是同步的。

3.1 SynchronizedList

SynchronizedList類是一個Collections類中包級私有的靜態內部類,咱們在編寫代碼的時候沒法直接調用這個類,只能經過Collection.synchronizedList()方法並傳入一個List來使用它,這個方法實際上就是幫咱們將原來沒有同步措施的普通List包裝成了SynchronizedList,使其擁有線程安全的特性,對其進行操做就是對原List的操做,以下所示:

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
複製代碼

3.2 Vector

Vector是JDK1.0就有的類,算是一個遠古時期的類了,在當時由於沒有比較好的同步工具,因此在併發場景下會使用到這個類,但如今隨着併發技術的進步,有了更好的同步工具類,因此Vector已經快成爲半廢棄狀態了。爲何呢?主要仍是由於同步效率過低,同步手段太粗暴了,粗暴到直接將絕大多數方法弄成同步方法(在方法上加入synchronized關鍵字),連clone方法都沒放過:

public synchronized Object clone() {
    try {
        @SuppressWarnings("unchecked")
            Vector<E> v = (Vector<E>) super.clone();
        v.elementData = Arrays.copyOf(elementData, elementCount);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}
複製代碼

這樣作雖然能確保線程安全,但效率實在過低了啊,尤爲是在競爭激烈的環境下,效率可能還不如單線程。相比之下,SynchronizedList就好不少,只是在必要的地方進行加鎖而已(不過實際上效率仍是挺低的)。基於它的CRUD操做就很少說了,和ArrayList沒什麼大的區別。

SynchronizedList和Vector的區別

他們最大區別就在同步效率,Vector的同步手段過於粗暴以致於效率過低,SynchronizedList的同步手段沒那麼粗暴,只是在有必要的地方進行同步而已,效率較Vector會好一些,但實際上也不會太好,比較同步手段比較單一,只是用內置鎖一種方案而已。

4 HashMap、HashTable和ConcurrentHashMap

當咱們想要存儲鍵值對或者說是想要表達某種映射關係的時候,都會用到HashMap這個類,HashTable則是HashMap的同步版本,是線程安全的,但效率很低,ConcurrentHashMap是JDK1.5以後替代HashTable的類,效率較高,因此如今在併發環境下通常再也不使用HashTable,而是使用ConcurrentHashMap。

順便說一下,ConcurrentHashMap是在J.U.C包下的,該包的主要做者是Doug Lea,這位大佬幾乎一我的撐起了Java併發技術。

4.1 HashMap

HashMap的內部結構是數組(稱做table)+鏈表(達到閾值會轉換成紅黑樹)的形式。數組和鏈表存儲的元素都是Node,以下所示:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey() { return key; }
    public final V getValue() { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
複製代碼

當向HashMap插入鍵值對的時候,會先拿key進行hash運算,獲得一個hashcode,而後根據hashcode來肯定該鍵值對(最終的形式上實際上是Node)應該放置在table的哪一個位置,這個過程當中若是有Hash衝突,即table中該位置已經有了Node節點A,那麼就會將這個新的鍵值對插入到以A節點爲頭節點的鏈表中(此尾插法,在JDK1.8中改成頭插法),若是在遍歷鏈表的途中遇到key相同的狀況,那麼就直接用新的value值替換到原來的值,這種狀況就再也不建立新的Node了,若是在途中沒有遇到的話,就在最後建立一個Node節點,並將其插入到鏈表末尾。

關於HashMap更多的內容,例如什麼併發擴容致使的問題,以及擴容因子對性能的影響等等,建議網上搜索,網上這樣的文章很是很是多,多到打開一個社區,都TM是將HashMap的文章.....

4.2 HashTable

HashTable的算法實現和HashMap並無太多區別,能夠簡單把HashTable理解成HashMap的線程安全版本,HashTable實現線程安全的手段也是很是粗暴的,和Vector幾乎同樣,直接將絕大多數方法設置成同步方法,以下所示:

public synchronized boolean contains(Object value) {
    if (value == null) {
        throw new NullPointerException();
    }

    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}
複製代碼

因此,其效率能夠說是很是低的,通常不多用了,而是使用接下來要講到的ConcurrentHashMap代替。

4.3 ConcurrentHashMap

該類位於java.util.concurrent(簡稱J.U.C)包下,是一款優秀的併發工具類。ConcurrentHashMap內部元素的存儲結構和HashMap幾乎同樣,都是數組+鏈表(達到閾值會轉換成紅黑樹)的結構。不一樣的是,CouncurrentHashMap是線程安全的,但並不像HashTable那樣粗暴的在每一個方法上加入synchronized內置鎖。而是採用一種叫作「分段鎖」的技術,將一整個table數組分紅多個段,每一個段有不一樣的鎖,每一個鎖只能影響到本身所在的段,對其餘段沒有影響,也就是說,在併發環境下,多個線程能夠同時對ConcurrentHashMap的不一樣的段進行操做。效果就是吞吐量提升了,效率也比HashTable高不少,但麻煩的是一些全局性變量不太好保證一致性,例如size。

關於ConcurrentHashMap更多的內容,仍是建議自行查找資料,網上有不少分析ConcurrentHashMap的優秀文章。

4.4 HashMap、HashTable和ConcurrentHashMap的區別

其實上面幾個小節都一直有比較,就在這裏總結一下:

  1. HashTable是HashMap的同步版本,但因爲同步手段太粗暴,效率較低,ConcurrentHashMap在JDK1.5以後出現,是HashTable的替代類,在此以前,若是想要保證HashMap的線程安全,要麼使用HashTable,要麼使用Collections.synchronizedMap來包裝HashMap,但這兩個方案的效率都比較低。
  2. 他們三者的實現方式幾乎同樣,內部存儲結構並無什麼差異。
  3. HashTable幾乎處於半廢棄的狀態,不建議在新項目中使用了,推薦使用ConcurrentHashMap。

5 Java8中Stream對集合類的加強

Java8中除了lambda表達式,最大的特性就是Stream流了。Stream API能夠將集合看作流,集合中的元素看作一個一個的流元素。這樣的抽象能夠將對集合的操做變得很簡單、清晰,例如在之前要想合併兩個集合,就不得不作建立一個新的集合,而後遍歷兩個集合將元素放入到新的集合中,但用流API的話就很是簡單了,只須要將兩個集合看作兩個流,直接將兩個流合成一個流便可。

Stream API還提供了不少高階函數用於操做流元素,流入map,reduce,filter等,下面是一個使用Stream API的示例:

public void streamTest() {
    Random random = new Random();
    List<Integer> integers = IntStream.generate(() -> random.nextInt(100))
            .limit(100).boxed()
            .collect(Collectors.toList());

    integers.stream().map(num -> num * 10)
            .filter(num -> num % 2 == 0)
            .forEach(System.out::println);
}
複製代碼

就這幾行代碼,實際上只能算是三行代碼,就實現了隨機生成元素放入list中,而且作了一個map操做和filter操做,還順帶遍歷了一下List。若是要用之前的方法,就不得不這樣寫:

public void originTest() {
    Random random = new Random();
    List<Integer> integers = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        integers.add(random.nextInt(100));
    }
    for (int i = 0; i < 100; i++) {
        integers.set(i, integers.get(i) * 10); //map
    }
    for (int i = 0; i < 100; i++) {
        if (integers.get(i) % 2 == 0)  //filter
            System.out.println(integers.get(i)); //foreach
    }
}
複製代碼

這三個for循環看起來實在是難看。這就是Stream API的優勢,簡潔,方便,抽象程度高,但可讀性會差一些,若是對lambda和Stream不熟悉的朋友第一次看到可能會比較費勁(但實際上,這是很簡單的代碼)。

那是否是之後對集合的操做都使用Stream API呢?別那麼極端,Stream API確實簡潔,但可讀性不好,Debug難度很是高,更多的時候是靠人肉Debug,並且性能上可能會低於傳統的方法,也有可能高,因此,個人建議是:在使用以前,最後先測試一下,將兩種方案對比一下,最終根據測試結果挑選一個比較好的方案。

6 小結

集合類是Java開發者必需要掌握的,經過閱讀源碼理解它們比看文章詳解來的更加深入。本文只是簡單的講了幾個經常使用的集合類,還有不少其餘的例如TreeMap,HashSet,TreeSet,Stack都沒有涉及,不是說這些集合類不重要,只是受篇幅限制,沒法一一道來,但願讀者能好好認真看看這些類的源碼,看源碼的時候不須要從頭看到尾,能夠先看幾個經常使用的方法,例如get,put等,而後一步一步跟進去,也可使用調試器單步跟蹤代碼。

相關文章
相關標籤/搜索