Java集合源碼淺析(一) : ArrayList

(尊重勞動成果,轉載請註明出處:https://yangwenqiang.blog.csdn.net/article/details/105418475冷血之心的博客)

背景

一直都有這麼一個打算,那就是將Java中常見集合的源碼進行一個全面的梳理(儘管已經有很多人進行了梳理總結)。重複造輪子的意義就在於,筆者可以親自去閱讀與欣賞JDK集合源碼,學習JDK設計者們那優雅的code風格。

那麼,就讓我們從常用的集合ArrayList來看起吧。(注. 本系列文章所使用的JDK版本爲1.8)

正文

ArrayList介紹

相信各位小夥伴都相當熟悉ArrayList的使用,不管是平時的日常工作,或者是面試時候的手撕算法中,我們都在頻繁的使用ArrayList(當然,還有LinkedList)。

當然,對於面試題目:“ArrayList和LinkedList有什麼區別?” 我們也可以倒背如流,分析的頭頭是道。

ArrayList和LinkedList的區別總結如下:

  • ArrayList底層使用了動態數組實現,實質上是一個動態數組
  • LinkedList底層使用了雙向鏈表實現,可當作堆棧、隊列、雙端隊列使用
  • ArrayList在隨機存取方面效率高於LinkedList
  • LinkedList在節點的增刪方面效率高於ArrayList
  • ArrayList必須預留一定的空間,當空間不足的時候,會進行擴容操作
  • LinkedList的開銷是必須存儲節點的信息以及節點的指針信息

接下來,我們先來依次分析ArrayList中關鍵性的源碼吧。

類關鍵信息解析

ArrayList的實現原理是動態數組,它是線程不安全的,允許其中元素爲null。ArrayList實現了 List, RandomAccess, Cloneable, java.io.Serializable接口,如下所示:
在這裏插入圖片描述
該類的繼承關係圖如下所示:
在這裏插入圖片描述

這裏,筆者突然覺得RandomAccess比較陌生,順勢點開去看了下這個接口是幹嘛用的?

答:其中RandomAccess代表了其擁有隨機快速訪問的能力,ArrayList可以以O(1)的時間複雜度去根據下標訪問元素。

JDK源碼中給出了這樣的例子:
在這裏插入圖片描述
舉個例子就是說我們的ArrayList實現了RandomAccess接口,表示具有了隨機訪問的能力,所以在遍歷的時候,使用for循環,隨機獲取遍歷速度更快;

而LinkedList沒有實現該接口,不具備隨機訪問能力,所以遍歷LinkedList的時候,我們優先使用迭代器來進行遍歷。

比如說這段代碼:

//        List<Integer> list = new ArrayList<>();
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(3);
        list.add(2);
        list.add(4);
        
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

LinkedList執行起來效率會很低,大家可以點開看看list.get(i)到底經歷了什麼鬼,後續章節我們也會解析。

關鍵屬性

既然,ArrayList底層實現是一個動態數組,那麼來看下都有哪些基本屬性吧。

// 用於序列化的版本號,這裏可以忽略
private static final long serialVersionUID = 8683452581122892189L;

    /集合的初始容量爲10
    private static final int DEFAULT_CAPACITY = 10;
    //指定大小的構造函數裏可能使用到的空數組
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //默認構造函數裏的空數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //真正存放元素的數組
    transient Object[] elementData;
    // 數組中真正包含的元素個數
    private int size;
    // 最大數組容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

至於說JDK8中爲什麼會有兩個默認空數組?大家可以參考下這篇文章的介紹:
https://blog.csdn.net/weixin_43390562/article/details/101236833
簡單理解就是說爲了避免創建太多的空數組,減少內存的消耗,所以整了一個全局的空數組出來。

構造方法

三種構造方法:

   // 默認無參構造方法,初始容量是10,在add元素的時候,纔會真正去初始化容量爲10
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    // 指定容量創建ArrayList,分爲容量大於0,等於0以及小於0,三種情況
    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);
        }
    }
    //將別的集合直接賦值進行創建的構造方法
    public ArrayList(Collection<? extends E> c) {
        //直接用toArray()的方法獲取集合的數組對象,並且直接賦值給elementData
        elementData = c.toArray();
        size = elementData.length;
        // 這裏是當c.toArray出錯,沒有返回Object[]時,利用Arrays.copyOf 來複制集合c中的元素到elementData數組中
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }


如何擴容?

既ArrayList底層是基於動態數組實現的,那必不可少的會有一個擴容的過程吧?講道理,應該是我們add元素的時候,是可能觸發擴容過程的,接下里我們先來看下擴容方法grow( )的實現吧。

    //擴容方法
    private void grow(int minCapacity) {
        // 記錄擴容前數組的長度
        int oldCapacity = elementData.length;
        //將原數組的長度擴大0.5倍作爲擴容後新數組的長度(如果擴容前數組長度爲10,那麼經過擴容後的數組長度應該爲15)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容後的長度小於當前數據量,那麼就將當前數據量的長度作爲本次擴容的長度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判斷新數組長度是否大於可分配數組的最大大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //將擴容長度設置爲最大可用長度
            newCapacity = hugeCapacity(minCapacity);
        // 拷貝,擴容,構建一個新的數組
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    //判斷如果新數組長度超過當前數組定義的最大長度時,就將擴容長度設置爲Interger.MAX_VALUE,也就是int的最大長度
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE : MAX_ARRAY_SIZE;
        }


常用API

這裏列出ArrayList中常用的API,後邊會對其源碼進行一定的淺析。

boolean add(E e)
將指定的元素添加到此列表的尾部。

void add(int index, E element)
將指定的元素插入此列表中的指定位置。

boolean addAll(Collection c)
按照指定 collection 的迭代器所返回的元素順序,將該 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection c)
從指定的位置開始,將指定 collection 中的所有元素插入到此列表中。

void clear()
移除此列表中的所有元素。

Object clone()
返回此 ArrayList 實例的淺表副本。

boolean contains(Object o)
如果此列表中包含指定的元素,則返回 truevoid ensureCapacity(int minCapacity)
如有必要,增加此 ArrayList 實例的容量,以確保它至少能夠容納最小容量參數所指定的元素數。

E get(int index)
返回此列表中指定位置上的元素。

int indexOf(Object o)
返回此列表中首次出現的指定元素的索引,或如果此列表不包含元素,則返回 -1boolean isEmpty()
如果此列表中沒有元素,則返回 true

int lastIndexOf(Object o)
返回此列表中最後一次出現的指定元素的索引,或如果此列表不包含索引,則返回 -1。

E remove(int index)
移除此列表中指定位置上的元素。

boolean remove(Object o)
移除此列表中首次出現的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)
移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之間的所有元素。

E set(int index, E element)
用指定的元素替代此列表中指定位置上的元素。

int size()
返回此列表中的元素數。

Object[] toArray()
按適當順序(從第一個到最後一個元素)返回包含此列表中所有元素的數組。

T[] toArray(T[] a)
按適當順序(從第一個到最後一個元素)返回包含此列表中所有元素的數組;返回數組的運行時類型是指定數組的運行時類型。

void trimToSize()
將此 ArrayList 實例的容量調整爲列表的當前大小。


add增加元素

從上邊列出的常見API中,我們可以看出當前存在四種add方法,我們先來看第一種:

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //判斷添加後的長度是否需要擴容
        ensureCapacityInternal(size + 1); 
        //在數組末尾添加上當前元素,並且修改size大小
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        // 判斷是否是第一次初始化數組
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    // 判斷擴容的方法
    private void ensureExplicitCapacity(int minCapacity) {
        // 如果需要擴容modCount++,此參數是指當前列表結構被修改的次數
        modCount++;
        // 判斷當前數據量是否大於數組的長度
        if (minCapacity - elementData.length > 0)
            // 如果大於則進行擴容操作
            grow(minCapacity);
    }

這塊需要注意的是DEFAULT_CAPACITY是初始容量,前面說了等於10,擴容grow方法我們在上邊也介紹過了。

接下來,是指定index的add方法,本質上沒有區別,只不過多了一個元素copy的過程,以及index check的邏輯:

	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++;
    }
	/**
     * A version of rangeCheck used by add and addAll.
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

然後就是添加一個完整集合的add方法:

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

指定index的add一個集合的方法:

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

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

這兩個方法也是一樣的,判斷是否需要擴容,然後進行了一個數組copy的過程。

add方法總結:

先判斷是否越界,是否需要擴容;如果擴容, 就複製數組;然後設置對應下標元素值。

擴容過程:

  • 如果需要擴容的話,默認擴容一半。
  • 如果擴容一半不夠,就用目標的size作爲擴容後的容量。
  • 在擴容成功後,會修改modCount

get方法獲取元素

既然是一個數組,那沒啥好說的,check一下index,然後直接獲取即可。

	/**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

set方法設置元素

先看源碼:

/**
     * Replaces the element at the specified position in this list with
     * the specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index);

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

注意,就是替換了index位置的元素值,但是這裏返回值是一個該位置上的oldValue


remove刪除元素

public E remove(int index) {
    rangeCheck(index);//判斷是否越界
    modCount++;//修改modeCount 因爲結構改變了
    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  //置空原尾部數據 不再強引用, 可以GC掉
    return oldValue;
}

這個刪除指定位置index上的元素還是很好理解的,還是涉及到了一個元素的copy。接下來,再看一個刪除指定Object的方法:

//刪除該元素在數組中第一次出現的位置上的數據。 如果有該元素返回true,如果false。
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);//根據index刪除元素
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
//不會越界 不用判斷 ,也不需要取出該元素。
private void fastRemove(int index) {
    modCount++;//修改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  //置空 不再強引用
}

刪除操作一定會修改modCount,且可能涉及到數組的複製,相對低效,並且在批量刪除中,涉及高效的保存兩個集合公有元素的算法

說了增(add)刪(remove)改(set)查(get)之後,我們再看看下其餘常見的API方法吧。

indexOf方法:

這個我們就不貼源碼了,感興趣的可以看看,很簡單,就是簡單遍歷找到該元素,找不到返回-1。另外,lastIndexOf方法則是從後開始找該元素。

contains方法

藉助於indexOf方法實現,源碼如下:

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

clear方法

/**
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

clear方法就是將數組中的所有位置都置爲null,並且會修改modCount。

size和isEmpty方法

/**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }

    /**
     * Returns <tt>true</tt> if this list contains no elements.
     *
     * @return <tt>true</tt> if this list contains no elements
     */
    public boolean isEmpty() {
        return size == 0;
    }

我們在前面介紹ArrayList屬性的時候,介紹到了size屬性,在add以及remove方法中會進行size++和size–。size直接記錄了當前實際存在於數組中的元素,所以可以直接返回。

toArray方法

/**
     * Returns an array containing all of the elements in this list
     * in proper sequence (from first to last element).
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this list.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this list in
     *         proper sequence
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    /**
     * Returns an array containing all of the elements in this list in proper
     * sequence (from first to last element); the runtime type of the returned
     * array is that of the specified array.  If the list fits in the
     * specified array, it is returned therein.  Otherwise, a new array is
     * allocated with the runtime type of the specified array and the size of
     * this list.
     *
     * <p>If the list fits in the specified array with room to spare
     * (i.e., the array has more elements than the list), the element in
     * the array immediately following the end of the collection is set to
     * <tt>null</tt>.  (This is useful in determining the length of the
     * list <i>only</i> if the caller knows that the list does not contain
     * any null elements.)
     *
     * @param a the array into which the elements of the list are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose.
     * @return an array containing the elements of the list
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this list
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

這個方法也是常用的,可以將當前的list方便的轉換爲一個數組,底層實現還是一個數組的copy

迭代器 Iterator

public Iterator<E> iterator() {
    return new Itr();
}
/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return //默認是0
    int lastRet = -1; // index of last element returned; -1 if no such  //上一次返回的元素 (刪除的標誌位)
    int expectedModCount = modCount; //用於判斷集合是否修改過結構的 標誌

    public boolean hasNext() {
        return cursor != size;//遊標是否移動至尾部
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)//判斷是否越界
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)//再次判斷是否越界,在 我們這裏的操作時,有異步線程修改了List
            throw new ConcurrentModificationException();
        cursor = i + 1;//遊標+1
        return (E) elementData[lastRet = i];//返回元素 ,並設置上一次返回的元素的下標
    }

    public void remove() {//remove 掉 上一次next的元素
        if (lastRet < 0)//先判斷是否next過
            throw new IllegalStateException();
        checkForComodification();//判斷是否修改過

        try {
            ArrayList.this.remove(lastRet);//刪除元素 remove方法內會修改 modCount 所以後面要更新Iterator裏的這個標誌值
            cursor = lastRet; //要刪除的遊標
            lastRet = -1; //不能重複刪除 所以修改刪除的標誌位
            expectedModCount = modCount;//更新 判斷集合是否修改的標誌,
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
//判斷是否修改過了List的結構,如果有修改,拋出異常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

迭代器其實是一個內部類,包括Itr和ListItr,如下所示:
在這裏插入圖片描述
那麼,這兩種迭代器有啥區別呢?我們一起來看下,Iterator和ListIterator的區別如下:

  • 前者可以遍歷list和set集合;後者只能用來遍歷list集合
  • 前者只能前向遍歷集合;後者可以前向和後向遍歷集合
  • 後者實現了前者,增加了一些新的功能。

感興趣的同學可以看下源碼,也在ArrayList中哦,是一個內部類

ensureCapacity

這是一個重要但是不常用的API,ensureCapacity可以保證將當前數組擴容到足夠容納指定的個數,減少程序中的自動擴容次數,接下來我們舉個例子:

ArrayList<Integer> list = new ArrayList<>();
        long begin1 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println("cost time2:"+(System.currentTimeMillis()-begin1));

        ArrayList<Integer> list2 = new ArrayList<>();
        list2.ensureCapacity(10000);
        long begin2 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            list2.add(i);
        }
        System.out.println("cost time2:"+(System.currentTimeMillis()-begin2));

上邊這段代碼,我們對list2在使用前提前進行了擴容,執行效率顯著提升。

cost time2:4
cost time2:1

內部實現如下,主要基於grow方法進行擴容實現的。

/**
     * Increases the capacity of this <tt>ArrayList</tt> instance, if
     * necessary, to ensure that it can hold at least the number of elements
     * specified by the minimum capacity argument.
     *
     * @param   minCapacity   the desired minimum capacity
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_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);
    }

ArrayList源碼總結

源碼實在是太多,我們通過對常用API的分析,大概可以得出ArrayList的底層實現原理。總結如下:

  • ArrayList內部基於動態數組實現,所以可以進行擴容操作,每一次擴容增量都是50%
  • 基於數組實現的,所以內部多個API都避免不了對數組的copy操作,比如set和remove操作,所以導致ArrayList插入和刪除效率低下
  • 基於數組實現,並且實現了RandomAccess,所以可以隨機訪問,根據index來找到元素的值,所以ArrayList獲取元素的效率很高
  • 提供了多個迭代器,都是基於內部類實現的
  • 底層源碼中沒有做同步處理,所以是線程不安全的,之前的版本Vector原理基本一直,但是Vector在方法的實現上都會加上synchronized關鍵字
  • modeCount會在適當的時候進行++操作,可以實現快速失敗

後續我會繼續更新集合源碼系列文章,歡迎大家關注交流~

如果對你有幫助,記得點贊哈,歡迎大家關注我的博客,關注公衆號(文強的技術小屋),學習更多技術知識,一起遨遊知識海洋~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章