1. 程式人生 > 實用技巧 >List介面下重要集合原始碼分析

List介面下重要集合原始碼分析

實現List介面的重要集合原始碼分析

簡介

List介面是類集Collection下一個比較重要的介面,它的下面有很多我們常用的實現類,而我們這次主要是介紹它的三個重要實現類!

  • ArrayList(底層資料結構是陣列,執行緒不安全)
  • LinkedList(底層資料結構是連結串列,執行緒不安全)
  • Vector(底層資料結構是陣列,執行緒安全)

ArrayList分析

ArrayList類擴充套件了AbstractList並實現了List介面。它支援隨需要而增長的動態陣列,也就是說可以動態地增加或減少其大小。它的底層是一個數組,建立的時候有著原始的大小,當超過了它的大小,類集自動增大。當物件被刪除後,陣列就可以縮小。

預設屬性

ArryList的預設有幾個屬性,其中包含了預設的陣列大小,定義為空返回的陣列,以及預設返回的陣列,還有實際陣列大小的變數。我們先理清楚就比較能明白後面的擴容機制

//陣列預設的大小
private static final int DEFAULT_CAPACITY = 10;
//當ArrayList指定陣列容量為0的時候,返回的陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
//預設返回的,只要不是指定為0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//儲存新增到ArrayList的元素,當第一次新增到ArrayList中的時候就會進行擴容
transient Object[] elementData;
//ArrayList的實際大小
private int size;

為了方便,transient Object[] elementData這個陣列下面我會簡稱為運算元組,也就是我們實際進行操作的。

構造方法

上面我們也瞭解到了ArrayList定義的幾個屬性,下面我們來介紹關於ArrayList初始化的時候三個構造方法!

第一個構造方法

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

我們可以指定ArrayList的初始大小,如果大於0就是先對我們用來新增元素的陣列進行初始化;如果為0就是我們上面上面說的,返回陣列容量為0的陣列;如果小於0,那就給你異常了呵呵。

第二個構造方法

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

這是一個空構造方法,我們的運算元組就用預設返回的陣列。

第三個構造方法

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

這是一個定義了構造一個包含指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排列的。?是任意類的意思,E是我們指定的型別,泛型,也就是繼承了Collection的集合。

我們將集合轉換成陣列,如果長度等於0還是返回容量為0的陣列,如果不是就呼叫Arrays.copyOf進行復制給我們的ArrayList運算元組。

常用方法分析

ArrayList的方法有很多,但是我們只要從常見的增刪改查一步一步分析,就可以瞭解了底層很多的實現機制!

增加方法

增加方法有兩個,一個是直接新增,一個是按角標進行新增!

直接新增 add(E e)

首先我們來看看它的增加方法,下面這個增加方法,就是直接增加了一個元素。它首先是嘗試容量+1,也就是分析對我們的容量有沒有必要加1,這是為了防止陣列越界!然後就是對我們的運算元組進行元素的新增,然後返回true!

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

可以看到我們用到了一個確認容量大小的方法,我們順著原始碼去看這個方法。可以看到,這是三個連串使用的方法!我們首先是進行了下面原始碼中的第二個方法,然後在方法體裡面呼叫了第一個方法並將返回結果作為第三個方法的引數,來呼叫第三個方法。

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
        }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

簡單的分析它們進行了什麼的操作

  1. 如果我們運算元組不等於預設陣列,返回我們實際大小加1,這是需要擴容了!如果等於的話,返回與預設容量較大的那個數,因為我們下面要進行判斷,我們如果要進行擴容總不能擴到比預設還小吧!
  2. 然後就是minCapacity - elementData.length > 0這一步,如果加1操作後的容量大於我們運算元組的長度的話,呼叫grow方法進行擴容!傳入的引數就是我們的最小需要的擴容大小!

那麼就到了我們的grow方法

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

我們進行了陣列的大小擴容,int newCapacity = oldCapacity + (oldCapacity >> 1);,預設就是擴容1.5倍!如果擴容後小於我們所需要的最小容量那麼直接就用我們的最小容量,下面同時還進行了一些邊界處理。最後呼叫我們的Arrays.copyOf進行對原來的運算元組進行復制到擴容後的新陣列!

hugeCapacity方法

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

至於為什麼最大陣列長度要設為為Integer.MAX_VALUE - 8,在後面面試題分析會介紹!

角標新增add(int index, E element)

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

首先進行我們呼叫了rangeCheckForAdd(index);進行了角標檢查,防止出現了越界等操作!如果就是呼叫了我們上面一樣的要不要嘗試容量加1,也就是判斷容量是否足夠!然後直接就進行了陣列的複製操作,直接複製大小為角標加1的資料部分,然後再進行陣列的新增元素和實際大小加1!

System.arraycopy(...)底層是native修飾的方法,也就是呼叫我們的C++函式來進行復制的!

移除方法

其實ArrayList的重要點也就是我們的擴容機制,其他的一些方法,但是可以通過閱讀原始碼可以輕鬆理解的。移除方法可以通過角標和元素來移除

通過角標

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

進行的操作有:

  • 呼叫rangeCheck邊界檢查
  • 獲取要刪除的元素,用於返回
  • 如果角標後面還有元素,對後面的元素進行復制,也就是後面的元素都向前移以為
  • 陣列實際大小-1,呼叫GC

通過元素

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

這個方法看起來很簡單,就是遍歷,如果找到與我們要刪除的元素相等的話,呼叫fastRemove方法,然後返回true;如果遍歷後沒有找到要刪除的元素的話,那麼就返回false,刪除失敗!所以我們下面具體看一下fastRemove方法。

    private void fastRemove(int index) {
        modCount++;
        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
    }

這個方法就是上面的remove的閹割版,實現的功能都一樣!

設定方法

檢查角標,替換舊的元素,並返回舊的元素。

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

查詢方法

檢查角標,如果沒有越界就返回該值。

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

        return elementData(index);
    }

面試題分析

這裡選了比較常見的三道面試題,通過我們上面的原始碼都可以輕鬆的分析了!

1、ArrayList擴容機制是怎麼樣的? 詳細說一下。

我們在上面分析了,首先是進行判斷一下是否需要容量加1操作,防止陣列越界,定義我們所需最少的容量大小!然後就是再判斷是否需要擴容。如果需要擴容的話,就會增加老陣列的1.5倍,如果增加後大小還是小於我們的所需要的最小容量,那麼擴容的大小就會改為我們的所需的最小容量大小!如果超過ArrayList允許的最大長度Integer.MAX_VALUE(),那麼新陣列長度為Integer.MAX_VALUE,否則為Integer.MAX_VALUE - 8。(為什麼需要進行-8呢?是因為物件標頭大小是不能超過8個位元組的,我們可以預設它就是8,所有留了這麼一個8位元組大小!具體的話可以看這裡

2、在索引中ArrayList的增加或者刪除某個物件的執行過程效率很低嗎?解釋一下為什麼?

效率是很低,因為我們也看到了,無論是刪除還是增加觸發擴容機制的話,都是在底層呼叫System.copyOf()方法進行陣列的複製或者移位操作。

  • 增加元素時,我們要把要增加位置及以後的所有元素都往後移一位,先騰出一個空間,然後再進行新增。
  • 刪除某個元素時,我們也要把刪除位置以後的元素全部元素往前挪一位,通過覆蓋的方式來刪除。

3、如何複製某個ArrayList到另一個ArrayList中去?

下面就是把某個ArrayList複製到另一個ArrayList中去的幾種技術:

  1. 使用clone()方法,比如ArrayList newArray = oldArray.clone();
  2. 使用ArrayList構造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
  3. 使用Collections的copy方法。

注意1和2是淺拷貝(shallow copy)。這裡方法1是所有Object物件都有的;方法2的構造方法我們也介紹了;方法3的Collections是一個工具類,集成了很多對集合的操作方法!

Vector分析

vector也是繼承AbstractList然後實現List介面的,跟ArrayList很像,和ArrayList裡面很多屬性和方法都一樣。所以這裡主要介紹它和ArrayList的區別。

區別1

Vector是執行緒安全的,ArrayList是執行緒不安全的!

Vector是如何實現執行緒安全的呢?它在所有容易被執行緒影響的方法都加了Synchronized關鍵字來修飾!所以我們可以把它看成是同步版的ArrayList。但也恰恰是因為進行了同步操作,所以它的效率是不如ArrayList的。

當然,如果我想要同步操作的話,不是一定要用Vector的,Colletions也提供了可以將ArrayList變成執行緒同步的方法

 List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());

但其實去看原始碼也可以發現,這個方法同樣是給list的方法加一層synchronized。

區別2

觸發擴容機制的時候,我們的ArrayList是1.5倍,而我們的Vector確是兩倍!

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

可以看到我們的4,5行程式碼,如果設定自動增長的長度就加上這個長度,如果沒有的話,預設就是加了原來的容量,也就是擴大為原來的兩倍!

總結

簡單總結一下,Vector與ArrayList大徑相似,最大的區別就是執行緒安全的區別了,記住這個就好了,反正現在也很少去用Vector了。同時,我們需要注意的,不管是Vector還是ArrayList,有時候是挺浪費記憶體的。因為有時候如果我有了5w容量了,但是我下面只會再用掉一個容量單位的話,我們的Vector會擴容到7w5,我們的Vector會擴容到10w,這記憶體浪費的有點多,所以有時候初始的時候就設定大小容量!

LinkedList分析

LinkedList實現了List介面,我們還需要關注的是它還實現了一個比較重要的介面就是Deque。所以我們的LinekdList是可以像操作佇列和棧一樣來操作。

LinkedList底層是一個雙向連結串列,我們同樣像分析ArrayList那樣對屬性和構造方法還有常用方法進行分析,來了解一下它的特性!

屬性

    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

三個屬性,我們的連結串列長度,頭結點,尾節點。

構造方法

我們有兩個構造方法,一個空構造方法public LinkedList() {},因為我們是連結串列,所以是不需要指定長度和容量的。還有一個就是有參構造,可以傳入一個Collection型別的子集合,後續會呼叫方法addAll進行連結串列的初始化。

    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

addAll會再呼叫另一個可以傳入連結串列長度的過載方法。然後程式碼主要是進行這些操作:

  • 檢查邊界,只能在大於等於0和小於等於連結串列長度的範圍才行

  • 進行判斷,如果要用來初始化的集合長度等於0,返回false

  • 如果判斷是否在尾結點插入,然後進行相關操作

  • 對陣列遍歷進行結點的插入

  • 連結串列的長度加上我們的陣列長度,返回true。

public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

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

        size += numNew;
        modCount++;
        return true;
    }

常用方法

LinkedList常用的方法太多了,很多都是連結串列的相關操作,可以看起來有點複雜,但是仔細去研讀原始碼,也就很好理解。這裡也就介紹一下增加刪除吧

增加

增加操作的話,可以在頭結點前面增加,也可以在我們的尾結點後面增加,預設的add方法是在尾結點後面增加。

  • add(E) 在尾結點後面插入,呼叫linkLast(e)方法
  • addLast(E)在尾結點後面插入,呼叫linkLast(e)方法
  • addFirst(E)在頭結點前面插入,呼叫linkFirst(e)方法
    /**
     * Links e as first element.
     */
    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++;
    }
    /**
     * Links e as last element.
     */
    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++;
    }

程式碼很好理解,具體操作解析的如下圖:

刪除

移除元素的話,有三個方法

  • remove(Object o),移除在連結串列中的元素
  • removeLast(),移除連結串列最後一個元素
  • removeFirst(),移除連結串列第一個元素

remove方法

我們先來看看remove方法

    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

首先是進行了元素的判空,然後進行了遍歷,然後都呼叫了unlink方法後返回true;如果沒有找到元素就返回false。

順藤摸瓜來看看unlink方法

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

這個方法呢,很普通,就是進行雙向連結串列的正常刪除操作,沒什麼好解釋的,如果不懂的話,可以看看下面這張圖。如果看了圖還是不能理解的話,需要去補充連結串列的知識了!

removeFirst()與removeLast()

這兩個方法,一個呼叫了unlinkFirst(Node<E> f),一個呼叫了unlinkLast(Node<E> l),也都是雙向連結串列的正常操作,沒有什麼特別難的地方!

LinkedList小總結

LinkedList的方法非常多,基本都是一些連結串列的操作。同時它也是執行緒不安全的,我們同樣可以用Collections幫助類對其進行轉換成執行緒安全的。

        LinkedList<Integer> linkedList = (LinkedList<Integer>) Collections.synchronizedList(new LinkedList<Integer>());

面試題分析

這裡我只找了一題,因為很多基本很多都只問這題。其他的話,通過上面的學習也可以說道說道!

請說明ArrayList和LinkedList的區別?

  • ArrayList是基於索引的資料介面,它的底層是陣列。它可以以O(1)時間複雜度對元素進行隨機訪問。與此對應,LinkedList是以元素列表的形式儲存它的資料,每一個元素都和它的前一個和後一個元素連結在一起,在這種情況下,查詢某個元素的時間複雜度是O(n)。
  • 相對於ArrayList,LinkedList的插入,新增,刪除操作速度更快,因為當元素被新增到集合任意位置的時候,不需要像陣列那樣重新計算大小或者是更新索引。
  • LinkedList比ArrayList更佔記憶體,因為LinkedList為每一個節點儲存了兩個引用,一個指向前一個元素,一個指向下一個元素。

(這裡再補充一下)

ArrayList增刪慢不是絕對的(在數量大的情況下,已測試):

  • 如果增加元素一直是使用add()(增加到末尾)的話,那是ArrayList要快
  • 一直刪除末尾的元素也是ArrayList要快【不用複製移動位置】
  • 至於如果刪除的是中間的位置的話,還是ArrayList要快

但一般來說:增刪多還是用LinkedList,因為上面的情況是極端的~

參考資料

Core Java

公眾號《Java 3y》文章

知乎專欄《Java那些事兒》