深入Java原始碼解析容器類List、Set、Map

執筆記憶的空白發表於2016-10-31

參考文獻:

  1. Java容器相關知識全面總結:http://www.codeceo.com/article/java-container-brief-introduction.html

  2. Java官方API文件:http://docs.oracle.com/javase/8/docs/api/

1 常用容器繼承關係圖

    先上一張網上的繼承關係圖

    個人覺得有些地方不是很準確,比如Iterator不是容器,只是一個操作遍歷集合的方法介面,所以不應該放在裡面。並且Map不應該繼承自Collection。所以自己整理了一個常用繼承關係圖如下:

    如上圖所示,接下去會自頂向下解釋重要的介面和實現類。

2 Collection和Map

    在Java容器中一共定義了2種集合, 頂層介面分別是Collection和Map。但是這2個介面都不能直接被實現使用,分別代表兩種不同型別的容器。

    簡單來看,Collection代表的是單個元素物件的序列,(可以有序/無序,可重複/不可重複 等,具體依據具體的子介面Set,List,Queue等);Map代表的是“鍵值對”物件的集合(同樣可以有序/無序 等依據具體實現)

2.1 Collection

    根據Java官方文件對Collection的解釋

    The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List. This interface is typically used to pass collections around and manipulate them where maximum generality is desired.

    大概意思就是:

    是容器繼承關係中的頂層介面。是一組物件元素組。有些容器允許重複元素有的不允許,有些有序有些無序。 JDK不直接提供對於這個介面的實現,但是提供繼承與該介面的子介面比如 List Set。這個介面的設計目的是希望能最大程度抽象出元素的操作。

    介面定義:

public interface Collection<E> extends Iterable<E> {

    ...

}

    泛型即該Collection中元素物件的型別,繼承的Iterable是定義的一個遍歷操作介面,採用hasNext next的方式進行遍歷。具體實現還是放在具體類中去實現。

    我們可以看下定義的幾個重要的介面方法

add(E e) 
 Ensures that this collection contains the specified element

clear()
 Removes all of the elements from this collection (optional operation).

contains(Object o)
 Returns true if this collection contains the specified element.

isEmpty()
 Returns true if this collection contains no elements.

iterator()
 Returns an iterator over the elements in this collection.

remove(Object o)
 Removes a single instance of the specified element from this collection, if it is present (optional operation).

retainAll(Collection<?> c)
 Retains only the elements in this collection that are contained in the specified collection (optional operation).(**ps:這個平時倒是沒注意,感覺挺好用的介面,保留指定的集合**)

size()
 Returns the number of elements in this collection.

toArray()
 Returns an array containing all of the elements in this collection.

toArray(T[] a)
 Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array.(**ps:這個介面也可以mark下**)

 ...

    上面定義的介面就代表了Collection這一類容器最基本的操作,包括了插入,移除,查詢等,會發現都是對單個元素的操作,Collection這類集合即元素物件的儲存。其中有2個介面平時沒用過但是覺得很有用

  1. retainAll(Collection<?> c) 保留指定的集合

  2. toArray(T[] a) 可以轉為陣列

2.2 Map

    Java官方文件對Map的解釋

    An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

    This interface takes the place of the Dictionary class, which was a totally abstract class rather than an interface.

    The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. Some map implementations, like the TreeMap class, make specific guarantees as to their order; others, like the HashMap class, do not.

    大概意思就是:

    一個儲存鍵值對映的物件。 對映Map中不能包含重複的key,每一個key最多對應一個value。

    這個介面替代了原來的一個抽象類Dictionary。

    Map集合提供3種遍歷訪問方法,1.獲得所有key的集合然後通過key訪問value。2.獲得value的集合。3.獲得key-value鍵值對的集合(key-value鍵值對其實是一個物件,裡面分別有key和value)。 Map的訪問順序取決於Map的遍歷訪問方法的遍歷順序。 有的Map,比如TreeMap可以保證訪問順序,但是有的比如HashMap,無法保證訪問順序。

    介面定義如下:

public interface Map<K,V> {

    ...

    interface Entry<K,V> {
        K getKey();
        V getValue();
        ...
    } 
}

    泛型分別代表key和value的型別。這時候注意到還定義了一個內部介面Entry,其實每一個鍵值對都是一個Entry的例項關係物件,所以Map實際其實就是Entry的一個Collection,然後Entry裡面包含key,value。再設定key不重複的規則,自然就演化成了Map。(個人理解)

    下面介紹下定義的3個遍歷Map的方法。

  1. SetkeySet()

    會返回所有key的Set集合,因為key不可以重複,所以返回的是Set格式,而不是List格式。(之後會說明Set,List區別。這裡先告訴一點Set集合內元素是不可以重複的,而List內是可以重複的) 獲取到所有key的Set集合後,由於Set是Collection型別的,所以可以通過Iterator去遍歷所有的key,然後再通過get方法獲取value。如下

    Map<String,String> map = new HashMap<String,String>();
    map.put("01", "zhangsan");
    map.put("02", "lisi");
    map.put("03", "wangwu");
    
    Set<String> keySet = map.keySet();//先獲取map集合的所有鍵的Set集合
    Iterator<String> it = keySet.iterator();//有了Set集合,就可以獲取其迭代器。
    
    while(it.hasNext()) {
         String key = it.next();
          String value = map.get(key);//有了鍵可以通過map集合的get方法獲取其對應的值。
         System.out.println("key: "+key+"-->value: "+value);//獲得key和value值
    }


  2. Collectionvalues()

    直接獲取values的集合,無法再獲取到key。所以如果只需要value的場景可以用這個方法。獲取到後使用Iterator去遍歷所有的value。如下

    Map<String,String> map = new HashMap<String,String>();
    map.put("01", "zhangsan");
    map.put("02", "lisi");
    map.put("03", "wangwu");
    
    Collection<String> collection = map.values();//返回值是個值的Collection集合
    System.out.println(collection);
  3. Set< Map.Entry< K, V>> entrySet()

    是將整個Entry物件作為元素返回所有的資料。然後遍歷Entry,分別再通過getKey和getValue獲取key和value。如下

Map<String,String> map = new HashMap<String,String>();
map.put("01", "zhangsan");
map.put("02", "lisi");
map.put("03", "wangwu");

//通過entrySet()方法將map集合中的對映關係取出(這個關係就是Map.Entry型別)
Set<Map.Entry<String, String>> entrySet = map.entrySet();
//將關係集合entrySet進行迭代,存放到迭代器中                
Iterator<Map.Entry<String, String>> it = entrySet.iterator();

while(it.hasNext()) {
     Map.Entry<String, String> me = it.next();//獲取Map.Entry關係物件me
      String key = me.getKey();//通過關係物件獲取key
      String value = me.getValue();//通過關係物件獲取value
}

    通過以上3種遍歷方式我們可以知道,如果你只想獲取key,建議使用keySet。如果只想獲取value,建議使用values。如果key value希望遍歷,建議使用entrySet。(雖然通過keySet可以獲得key再間接獲得value,但是效率沒entrySet高,不建議使用這種方法)

3 List、Set和Queue

    在Collection這個整合鏈中,我們介紹List、Set和Queue。其中會重點介紹List和Set以及幾個常用實現class。Queue平時實在沒用過。

    先簡單概述下List和Set。他們2個是繼承Collection的子介面,就是說他們也都是負責儲存單個元素的容器。但是最大的區別如下

  1. List是儲存的元素容器是有個有序的可以索引到元素的容器,並且裡面的元素可以重複。

  2. Set裡面和List最大的區別是Set裡面的元素物件不可重複。

3.1 List

    Java文件中介紹

    An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

    Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.

    …

    The List interface provides a special iterator, called a ListIterator, that allows element insertion and replacement, and bidirectional access in addition to the normal operations that the Iterator interface provides. A method is provided to obtain a list iterator that starts at a specified position in the list.

    大概意思是:

    一個有序的Collection(或者叫做序列)。使用這個介面可以精確掌控元素的插入,還可以根據index獲取相應位置的元素。

    不像Set,list允許重複元素的插入。有人希望自己實現一個list,禁止重複元素,並且在重複元素插入的時候丟擲異常,但是我們不建議這麼做。

    List提供了一種特殊的iterator遍歷器,叫做ListIterator。這種遍歷器允許遍歷時插入,替換,刪除,雙向訪問。 並且還有一個過載方法允許從一個指定位置開始遍歷。

    然後我們再看下List介面新增的介面,會發現add,get這些都多了index引數,說明在原來Collection的基礎上,List是一個可以指定索引,有序的容器。在這注意以下新增的2個新Iteractor方法。


    我們再看ListIterator的程式碼:


    一個集合在遍歷過程中進行插入刪除操作很容易造成錯誤,特別是無序佇列,是無法在遍歷過程中進行這些操作的。但是List是一個有序集合,所以在這實現了一個ListIteractor,可以在遍歷過程中進行元素操作,並且可以雙向訪問。

這個是之前開發中一直沒有發現的,好東西。mark

    以上就是List的基本概念和規則,下面我們介紹2個常用List的實現類,ArrayList和LinkedList。

3.1.1 ArrayList

    就Java文件的解釋,整理出以下幾點特點:

  1. ArrayList是一個實現了List介面的可變陣列

  2. 可以插入null

  3. 它的size, isEmpty, get, set, iterator,add這些方法的時間複雜度是O(1),如果add n個資料則時間複雜度是O(n).

  4. ArrayList不是synchronized的。

    然後我們來簡單看下ArrayList原始碼實現。這裡只寫部分原始碼分析。

    所有元素都是儲存在一個Object陣列中,然後通過size控制長度。

transient Object[] elementData;private int size;

    這時候看下add的程式碼分析


    其實在每次add的時候會判斷資料長度,如果不夠的話會呼叫Arrays.copyOf,複製一份更長的陣列,並把前面的資料放進去。

    我們再看下remove的程式碼是如何實現的。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

    其實就是直接使用System.arraycopy把需要刪除index後面的都往前移一位然後再把最後一個去掉。

PS:終於發現以前學習的資料結構用到用場了。O。O

3.1.2 LinkedList

    LinkedList是一個連結串列維護的序列容器。和ArrayList都是序列容器,一個使用陣列儲存,一個使用連結串列儲存。

    陣列和連結串列2種資料結構的對比:

  1. 查詢方面。陣列的效率更高,可以直接索引出查詢,而連結串列必須從頭查詢。

  2. 插入刪除方面。特別是在中間進行插入刪除,這時候連結串列體現出了極大的便利性,只需要在插入或者刪除的地方斷掉鏈然後插入或者移除元素,然後再將前後鏈重新組裝,但是陣列必須重新複製一份將所有資料後移或者前移。

  3. 在記憶體申請方面,當陣列達到初始的申請長度後,需要重新申請一個更大的陣列然後把資料遷移過去才行。而連結串列只需要動態建立即可。

    如上LinkedList和ArrayList的區別也就在此。根據使用場景選擇更加適合的List。

    下面簡單展示LinkedList的部分原始碼解析。

    首先是連結串列的節點的定義,非常簡單的一個雙向連結串列。

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;
    }
}

    然後每個LinkedList中會持有連結串列的頭指標和尾指標

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

    列舉最基本的插入和刪除的連結串列操作

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

    上面6個方法就是連結串列的核心,頭尾中間插入,頭尾中間刪除。其他對外的呼叫都是圍繞這幾個方法進行操作的

    同時LinkedList還實現了Deque介面,Deque介面是繼承Queue的。所以LinkedList還支援佇列的pop,push,peek操作。

總結

List實現使用場景資料結構
ArrayList陣列形式訪問List鏈式集合資料,元素可重複,訪問元素較快陣列
LinkedList連結串列方式的List鏈式集合,元素可重複,元素的插入刪除較快雙向連結串列

3.2 Set

    Set的核心概念就是集合內所有元素不重複。在Set這個子介面中沒有在Collection特別實現什麼額外的方法,應該只是定義了一個Set概念。下面我們來看Set的幾個常用的實現HashSet、LinkedHashSet、TreeSet

3.2.1 HashSet

    HashSet的核心概念。Java文件中描述

    This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.

    大概意思是:

    HashSet實現了Set介面,基於HashMap進行儲存。遍歷時不保證順序,並且不保證下次遍歷的順序和之前一樣。HashSet中允許null元素。

    進入到HashSet原始碼中我們發現,所有資料儲存在:

private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

    意思就是HashSet的集合其實就是HashMap的key的集合,然後HashMap的val預設都是PRESENT。HashMap的定義即是key不重複的集合。使用HashMap實現,這樣HashSet就不需要再實現一遍。

    所以所有的add,remove等操作其實都是HashMap的add、remove操作。遍歷操作其實就是HashMap的keySet的遍歷,舉例如下

...
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

public boolean contains(Object o) {
    return map.containsKey(o);
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public void clear() {
    map.clear();
}
...

3.2.2 LinkedHashSet

    LinkedHashSet的核心概念相對於HashSet來說就是一個可以保持順序的Set集合。HashSet是無序的,LinkedHashSet會根據add,remove這些操作的順序在遍歷時返回固定的集合順序。這個順序不是元素的大小順序,而是可以保證2次遍歷的順序是一樣的。

    類似HashSet基於HashMap的原始碼實現,LinkedHashSet的資料結構是基於LinkedHashMap。過多的就不說了。

3.2.3 TreeSet

    TreeSet即是一組有次序的集合,如果沒有指定排序規則Comparator,則會按照自然排序。(自然排序即e1.compareTo(e2) == 0作為比較)

    注意:TreeSet內的元素必須實現Comparable介面。

    TreeSet原始碼的演算法即基於TreeMap,具體演算法在說明TreeMap的時候進行解釋。

總結

Set實現使用場景資料結構
HashSet無序的、無重複的資料集合基於HashMap
LinkedSet維護次序的HashSet基於LinkedHashMap
TreeSet保持元素大小次序的集合,元素需要實現Comparable介面基於TreeMap

4 HashMap、LinkedHashMap、TreeMap和WeakHashMap

4.1 HashMap

    HashMap就是最基礎最常用的一種Map,它無序,以雜湊表的方式進行儲存。之前提到過,HashSet就是基於HashMap,只使用了HashMap的key作為單個元素儲存。

    HashMap的訪問方式就是繼承於Map的最基礎的3種方式,詳細見上。在這裡我具體分析一下HashMap的底層資料結構的實現。

在看HashMap原始碼前,先理解一下他的儲存方式-雜湊表(雜湊表)。像之前提到過的用陣列儲存,用連結串列儲存。雜湊表是使用陣列和連結串列的組合的方式進行儲存。(具體雜湊表的概念自行搜尋)如下圖就是HashMap採用的儲存方法。


hash得到數值,放到陣列中,如果遇到衝突則以連結串列方式掛在下方。

    HashMap的儲存定義是

transient Node<K,V>[] table;

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

    陣列table存放元素,如果遇到衝突下掛到衝突元素的next連結串列上。

    在這我們可以看下get核心方法和put核心方法的原始碼

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

    上面程式碼中看出先根據hash值和陣列長度作且運算得出下標索引。如果存在判斷hash值是否完全一致,如果不完全一致則next連結串列向下找一致的hash值。


    上面是put的核心原始碼,即查詢hash值所在索引是否有元素,沒有的話new一個Node直接放在table中。如果已經有Node了,就遍歷該Node的next,將新元素放到最後。

    HashMap的遍歷,是從陣列遍歷第一個非空的元素,然後再根據這個元素訪問其next下的所有Node。因為第一個元素不是一定從陣列的0開始,所以HashMap是無序遍歷。

4.2 LinkedHashMap

    LinkedHashMap相對於HashMap來說區別是,LinkedHashMap遍歷的時候具有順序,可以儲存插入的順序,(還可以設定最近訪問的元素也放在前面,即LRU)

    其實LinkedHashMap的儲存還是跟HashMap一樣,採用雜湊表方法儲存,只不過LinkedHashMap多維護了一份head,tail連結串列。


    即在建立新Node的時候將新Node放到最後,這樣遍歷的時候不再像HashMap一樣,從陣列開始判斷第一個非空元素,而是直接從表頭進行遍歷。這樣即滿足有序遍歷。

4.3 TreeMap

    TreeMap平時用的不多,TreeMap會實現SortMap介面,定義一個排序規則,這樣當遍歷TreeMap的時候,會根據規定的排序規則返回元素。

4.4 WeakHashMap

    WeakHashMap,此種Map的特點是,當除了自身有對key的引用外,此key沒有其他引用那麼此map會自動丟棄此值,

    舉例:宣告瞭兩個Map物件,一個是HashMap,一個是WeakHashMap,同時向兩個map中放入a、b兩個物件,當HashMap  remove掉a 並且將a、b都指向null時,WeakHashMap中的a將自動被回收掉。出現這個狀況的原因是,對於a物件而言,當HashMap  remove掉並且將a指向null後,除了WeakHashMap中還儲存a外已經沒有指向a的指標了,所以WeakHashMap會自動捨棄掉a,而對於b物件雖然指向了null,但HashMap中還有指向b的指標,所以
WeakHashMap將會保留。

    WeakHashMap用的也不多,在這簡單提及。

總結

Map實現使用場景資料結構
HashMap雜湊表儲存鍵值對,key不重複,無序雜湊雜湊表
LinkedHashMap是一個可以記錄插入順序和訪問順序的HashMap儲存方式是雜湊雜湊表,但是維護了頭尾指標用來記錄順序
TreeMap具有元素排序功能紅黑樹
WeakHashMap弱鍵對映,對映之外無引用的鍵,可以被垃圾回收雜湊雜湊表

結尾

    以上就是對於Java集合的完整分析和原始碼解析。其中ArrayList、HashMap使用較多,當考慮到效率時記得有Linded系列集合和WeakHashMap。Over~~

原文轉自:http://www.jianshu.com/p/047e33fdefd2

相關文章