1. 程式人生 > >Java 集合框架 原始碼淺析 與理解

Java 集合框架 原始碼淺析 與理解

最近在研究java原始碼,就是看一看別人寫好的東西,也不算是研究。知根知底的對以後的學習會有很大的幫助,我先去了解一下java集合框架,從總體上對這個組織和操作資料的資料結構有個淺顯得的瞭解。

從網上看了很多資料,發現這一張圖總結的還算不錯就引用過來了。但是最上面的Map和Collection之間的關係應該是依賴,不是Produces。

一、java集合框架概述

從上面的集合框架圖可以看到,Java集合框架主要包括兩種型別的容器.

一種是集合(Collection),儲存一個元素集合,另一種是圖(Map),儲存鍵/值對對映。Collection介面又有3種子型別,List、Set和Queue,再下面是一些抽象類,最後是具體實現類,常用的有ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap等等.

二、Collection介面

首先看一下Collection的結構:

Collection介面是處理物件集合的根介面,其中定義了很多對元素進行操作的方法,AbstractCollection是提供Collection部分實現的抽象類。上圖展示了Collection介面中的全部方法。

有幾個比較常用的方法,比如方法:

  • add()新增一個元素到集合中,
  • addAll()將指定集合中的所有元素新增到集合中,
  • contains()方法檢測集合中是否包含指定的元素,
  • toArray()方法返回一個表示集合的陣列。

Collection介面有三個子介面,下面詳細介紹。

1.List

List介面擴充套件自Collection,它可以定義一個允許重複的有序集合,從List介面中的方法來看,List介面主要是增加了面向位置的操作,允許在指定位置上操作元素,同時增加了一個能夠雙向遍歷線性表的新列表迭代器ListIterator。AbstractList類提供了List介面的部分實現,AbstractSequentialList擴充套件自AbstractList,主要是提供對連結串列的支援。下面介紹List介面的兩個重要的具體實現類,也是我們可能最常用的類,ArrayList和LinkedList。

ArrayList

通過檢視ArrayList的原始碼,我們可以很清楚地看到裡面的邏輯,它是用陣列儲存元素的,這個陣列可以動態建立,如果元素個數超過了陣列的容量,那麼就建立一個更大的新陣列(可以看出預設是10個),並將當前陣列中的所有元素都複製到新陣列中。假設第一次是集合沒有任何元素,下面以插入一個元素為例看看原始碼的實現。

1、方法add(int index, E element) 向集合中指定位置新增指定元素。
  public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
2、此方法主要是確定將要建立的陣列大小。
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
3、之後是建立陣列,可以明顯的看到先是確定了新增元素後的大小之後將元素複製到新陣列中。
  private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
4、最後是處理陣列,System.arraycopy()可以使用它來實現陣列之間的複製,將元素複製到新陣列中。
/**
     * The char[] specialized version of arraycopy().
     *
     * @hide internal use only
     */
    public static void arraycopy(char[] src, int srcPos, char[] dst, int dstPos, int length) {
        if (src == null) {
            throw new NullPointerException("src == null");
        }
        if (dst == null) {
            throw new NullPointerException("dst == null");
        }
        if (srcPos < 0 || dstPos < 0 || length < 0 ||
            srcPos > src.length - length || dstPos > dst.length - length) {
            throw new ArrayIndexOutOfBoundsException(
                "src.length=" + src.length + " srcPos=" + srcPos +
                " dst.length=" + dst.length + " dstPos=" + dstPos + " length=" + length);
        }
        if (length <= ARRAYCOPY_SHORT_CHAR_ARRAY_THRESHOLD) {
            // Copy char by char for shorter arrays.
            if (src == dst && srcPos < dstPos && dstPos < srcPos + length) {
                // Copy backward (to avoid overwriting elements before
                // they are copied in case of an overlap on the same
                // array.)
                for (int i = length - 1; i >= 0; --i) {
                    dst[dstPos + i] = src[srcPos + i];
                }
            } else {
                // Copy forward.
                for (int i = 0; i < length; ++i) {
                    dst[dstPos + i] = src[srcPos + i];
                }
            }
        } else {
            // Call the native version for longer arrays.
            arraycopyCharUnchecked(src, srcPos, dst, dstPos, length);
        }
    }  

LinkedList

LinkedList是在一個連結串列中儲存元素。

在學習資料結構的時候,我們知道連結串列和陣列的最大區別在於它們對元素的儲存方式的不同導致它們在對資料進行不同操作時的效率不同,同樣,ArrayList與LinkedList也是如此,實際使用中我們需要根據特定的需求選用合適的類,如果除了在末尾外不能在其他位置插入或者刪除元素,那麼ArrayList效率更高,如果需要經常插入或者刪除元素,就選擇LinkedList。

2.Set

Set介面擴充套件自Collection,它與List的不同之處在於,規定Set的例項不包含重複的元素。在一個規則集內,一定不存在兩個相等的元素。AbstractSet是一個實現Set介面的抽象類,Set介面有三個具體實現類,分別是雜湊集HashSet、鏈式雜湊集LinkedHashSet和樹形集TreeSet。

HashSet

雜湊集HashSet是一個用於實現Set介面的具體類,可以使用它的無參構造方法來建立空的雜湊集,也可以由一個現有的集合建立雜湊集。在雜湊集中,有兩個名詞需要關注,初始容量和客座率。客座率是確定在增加規則集之前,該規則集的飽滿程度,當元素個數超過了容量與客座率的乘積時,容量就會自動翻倍。

下面看一個HashSet的例子。

import java.util.HashSet;
import java.util.Set;
/**
 * @author ShanCanCan
 */
public class HashSetTest {
    public static void main(String[] args) {

        Set<String> set = new HashSet<>();

        set.add("11111");
        set.add("22222");
        set.add("33333");
        set.add("44444");
        set.add("22222");
        set.add("99999");
        set.add("00000");

        System.out.println(set.size());

        for (String e : set) {
            System.out.println(e);
        }

    }
}

看一下輸出結果:

從輸出結果我們可以看到,規則集裡最後有6個元素,而且在輸出時元素還是無序的。

LinkedHashSet

LinkedHashSet是用一個連結串列實現來擴充套件HashSet類,它支援對規則集內的元素排序。HashSet中的元素是沒有被排序的,而LinkedHashSet中的元素可以按照它們插入規則集的順序提取。

TreeSet

TreeSet擴充套件自AbstractSet,並實現了NavigableSet,AbstractSet擴充套件自AbstractCollection,樹形集是一個有序的Set,其底層是一顆樹,這樣就能從Set裡面提取一個有序序列了。在例項化TreeSet時,我們可以給TreeSet指定一個比較器Comparator來指定樹形集中的元素順序。樹形集中提供了很多便捷的方法。

下面是一個TreeSet的例子。

import java.util.TreeSet;
  /**
  * @author ShanCanCan
  */
public class TreeSetTest {
    public static void main(String[] args) {

        TreeSet<Integer> set = new TreeSet<>();

        set.add(1111);
        set.add(2222);
        set.add(3333);
        set.add(4444);
        set.add(5555);

        System.out.println(set.first()); // 輸出第一個元素
        System.out.println(set.lower(3333)); // 小於3333的最大元素
        System.out.println(set.higher(2222)); // 大於2222的最大元素
        System.out.println(set.floor(3333)); // 不大於3333的最大元素
        System.out.println(set.ceiling(3333)); // 不小於3333的最大元素

        System.out.println(set.pollFirst()); // 刪除第一個元素
        System.out.println(set.pollLast()); // 刪除最後一個元素
        System.out.println(set);
    }
}

看一下輸出結果:

3.Queue

佇列是一種先進先出的資料結構,元素在佇列末尾新增,在佇列頭部刪除。Queue介面擴充套件自Collection,並提供插入、提取、檢驗等操作。

上圖中,方法offer表示向佇列新增一個元素,poll()與remove()方法都是移除佇列頭部的元素,兩者的區別在於如果佇列為空,那麼poll()返回的是null,而remove()會丟擲一個異常。方法element()與peek()主要是獲取頭部元素,不刪除。

介面Deque,是一個擴充套件自Queue的雙端佇列,它支援在兩端插入和刪除元素,因為LinkedList類實現了Deque介面,所以通常我們可以使用LinkedList來建立一個佇列。PriorityQueue類實現了一個優先佇列,優先佇列中元素被賦予優先順序,擁有高優先順序的先被刪除。

下面是一個Queue的例子。

import java.util.LinkedList;
import java.util.Queue;
/**
 * @author ShanCanCan
 */
public class QueueTest {
    public static void main(String[] args) {

        Queue<String> queue = new LinkedList<>();

        queue.offer("aaaa");
        queue.offer("bbbb");
        queue.offer("cccc");
        queue.offer("dddd");
        queue.offer("eeee");
        queue.offer("ffff");

        while (queue.size() > 0) {
            System.out.println(queue.remove() + "");
        }
    }
}

看一下輸出結果:

三、Map介面

Map,圖,是一種儲存鍵值對對映的容器類,在Map中鍵可以是任意型別的物件,但不能有重複的鍵,每個鍵都對應一個值,真正儲存在圖中的是鍵值構成的條目。

下面是介面Map的類結構。

從上面這張圖中我們可以看到介面Map提供了很多查詢、更新和獲取儲存的鍵值對的方法,更新包括方法clear()、put()、putAll()、remove()等等,查詢方法包括containsKey、containsValue等等。Map介面常用的有三個具體實現類,分別是HashMap、LinkedHashMap、TreeMap。

1.HashMap

HashMap是基於雜湊表的Map介面的非同步實現,繼承自AbstractMap,AbstractMap是部分實現Map介面的抽象類。在平時的開發中,HashMap的使用還是比較多的。我們知道ArrayList主要是用陣列來儲存元素的,LinkedList是用連結串列來儲存的,那麼HashMap的實現原理是什麼呢?先看下面這張圖:

在之前的版本中,HashMap採用陣列+連結串列實現,即使用連結串列處理衝突,同一hash值的連結串列都儲存在一個連結串列裡。但是當連結串列中的元素較多,即hash值相等的元素較多時,通過key值依次查詢的效率較低。而JDK1.8中,HashMap採用陣列+連結串列+紅黑樹實現,當連結串列長度超過閾值(8)時,將連結串列轉換為紅黑樹,這樣大大減少了查詢時間。

下面主要通過原始碼介紹一下它的實現原理。

HashMap儲存元素的陣列

transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

陣列的元素型別是HashMapEntry

/** @hide */  // Android added.
    static class HashMapEntry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        HashMapEntry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

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

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

接下來我們看下HashMap的put操作。

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
     /**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

接下來我們看下HashMap的get操作。

 /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    /**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    /**
     * Returns <tt>true</tt> if this map contains a mapping for the
     * specified key.
     *
     * @param   key   The key whose presence in this map is to be tested
     * @return <tt>true</tt> if this map contains a mapping for the specified
     * key.
     */
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

到這裡HashMap的大致實現原理應該很清楚了,有幾個需要關注的重點是:HashMap儲存元素的方式以及根據Hash值確定對映在陣列中的位置還有JDK 1.8之後加入的紅黑樹的。

在HashMap中要找到某個元素,需要根據key的hash值來求得對應陣列中的位置。對於任意給定的物件,只要它的hashCode()返回值相同,那麼程式呼叫hash(int h)方法所計算得到的hash碼值總是相同的。我們首先想到的就是把hash值對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在HashMap中,(n - 1) & hash 用於計算物件應該儲存在table陣列的哪個索引處。HashMap底層陣列的長度總是2的n次方,當陣列長度為2的n次冪的時候,(n - 1) & hash 算得的index相同的機率較小,資料在陣列上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢效率也就較高了。

2.LinkedHashMap

LinkedHashMap繼承自HashMap,它主要是用連結串列實現來擴充套件HashMap類,HashMap中條目是沒有順序的,但是在LinkedHashMap中元素既可以按照它們插入圖的順序排序,也可以按它們最後一次被訪問的順序排序。

3.TreeMap

TreeMap基於紅黑樹資料結構的實現,鍵值可以使用Comparable或Comparator介面來排序。TreeMap繼承自AbstractMap,同時實現了介面NavigableMap,而介面NavigableMap則繼承自SortedMap。SortedMap是Map的子介面,使用它可以確保圖中的條目是排好序的。

在實際使用中,如果更新圖時不需要保持圖中元素的順序,就使用HashMap,如果需要保持圖中元素的插入順序或者訪問順序,就使用LinkedHashMap,如果需要使圖按照鍵值排序,就使用TreeMap。

四、其它集合類

上面主要對Java集合框架作了詳細的介紹,包括Collection和Map兩個介面及它們的抽象類和常用的具體實現類,下面主要介紹一下其它幾個特殊的集合類,Vector、Stack、HashTable、ConcurrentHashMap以及CopyOnWriteArrayList。

1.Vector

前面我們已經提到,Java設計者們在對之前的容器類進行重新設計時保留了一些資料結構,其中就有Vector。用法上,Vector與ArrayList基本一致,不同之處在於Vector使用了關鍵字synchronized將訪問和修改向量的方法都變成同步的了,所以對於不需要同步的應用程式來說,類ArrayList比類Vector更高效。

2.Stack

Stack,棧類,是Java2之前引入的,繼承自類Vector。

3.HashTable

HashTable和前面介紹的HashMap很類似,它也是一個散列表,儲存的內容是鍵值對對映,不同之處在於,HashTable是繼承自Dictionary的,HashTable中的函式都是同步的,這意味著它也是執行緒安全的,另外,HashTable中key和value都不可以為null。

上面的三個集合類都是在Java2之前推出的容器類,可以看到,儘管在使用中效率比較低,但是它們都是執行緒安全的。下面介紹兩個特殊的集合類。

4.ConcurrentHashMap

Concurrent,併發,從名字就可以看出來ConcurrentHashMap是HashMap的執行緒安全版。同HashMap相比,ConcurrentHashMap不僅保證了訪問的執行緒安全性,而且在效率上與HashTable相比,也有較大的提高。關於ConcurrentHashMap的設計,我將會在下一篇關於併發程式設計的部落格中介紹,敬請關注。

5.CopyOnWriteArrayList

CopyOnWriteArrayList,是一個執行緒安全的List介面的實現,它使用了ReentrantLock鎖來保證在併發情況下提供高效能的併發讀取。

五、總結

到這裡,對於Java集合框架的總結就結束了,還有很多集合類沒有在這裡提到,更多的還是需要大家自己去查去用。通過閱讀原始碼,查閱資料,收穫很大。

  • Java集合框架主要包括Collection和Map兩種型別。其中Collection又有3種子型別,分別是List、Set、Queue。Map中儲存的主要是鍵值對對映。

  • 規則集Set中儲存的是不重複的元素,線性表中儲存可以包括重複的元素,Queue佇列描述的是先進先出的資料結構,可以用LinkedList來實現佇列。效率上,規則集比線性表更高效。

  • ArrayList主要是用陣列來儲存元素,LinkedList主要是用連結串列來儲存元素,HashMap的底層實現主要是藉助陣列+連結串列+紅黑樹來實現。

  • Vector、HashTable等集合類效率比較低但都是執行緒安全的。包java.util.concurrent下包含了大量執行緒安全的集合類,效率上有較大提升。

本文引用了簡書文章,但是對裡面的內容做了很多更新,原始碼均來自jdk1.8.0_121。可以給你帶來更新,更好的理解。