JDK源碼解析集合篇--ArrayList全解析

對於一個集合的使用,我們首先關注的是:
1、 增刪改查的特點(時間複雜度是怎樣的) 適用於隨機訪問
2、是否允許空 允許爲空
3、是否允許重複數據 允許
4、是否有序,有序的意思是讀取數據的順序和存放數據的順序是否一致 有序
5、是否線程安全 非線程安全

ArrayList實現原理

ArrayList就是一個以數組形式實現的集合,但是它實現了長度可變。我們可以看其源碼屬性:
這裏寫圖片描述
ArrayList是基於數組的一個實現,elementData就是底層的數組,size:ArrayList裏面元素的個數,這裏要注意一下,size是按照調用add、remove方法的次數進行自增或者自減的,所以add了一個null進入ArrayList,size也會加1 。其初始容量爲10,當大於10時,進行擴容,在後邊詳細分析。

添加元素

當我們向一個arraylist添加一個元素時:我們直接看其源碼,是怎麼操作的:

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

上面代碼中有ensureCapacityInternal方法,是確保容量夠用,若不夠用就擴容。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //將現在的容量與我們需要的最小容量size + 1進行比較,選大的
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //看是否需要擴容
        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;  //改變modCount  是爲迭代器拋出異常做判斷的變量

        // overflow-conscious code
        //如果現有的容量不滿足需要的最小容量,需要擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

擴容,這也就是爲什麼一直說ArrayList的底層是基於動態數組實現的原因。本文是基於JDK1.8的,具體的:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //原先的容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);  //容量變爲原來的1.5倍 
        //是否滿足需要的最小容量,若不滿足,則擴容爲需要的最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //是否是在MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 內,不在的話設未MAX_ARRAY_SIZE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //將原數組元素拷貝到新的擴容後的數組中    是利用System.arraycopy實現的
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

這裏要注意擴容後的新容量爲什麼要變爲原來的1.5倍:這是因爲擴容擴多少,是JDK開發人員在時間、空間上做的一個權衡,提供出來的一個比較合理的數值。
1、如果一次性擴容擴得太大,必然造成內存空間的浪費
2、如果一次性擴容擴得不夠,那麼下一次擴容的操作必然比較快地會到來,這會降低程序運行效率,要知道擴容還是比價耗費性能的一個操作。
添加操作還有一個方法,可以做到在任意位置添加:

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

看到插入的時候,按照指定位置,把從指定位置開始的所有元素利用System.arraycopy方法做一個整體的複製,向後移動一個位置(當然先要用ensureCapacity方法進行判斷,加了一個元素之後數組會不會不夠大),然後指定位置的元素設置爲需要插入的元素,完成了一次插入的操作。
此複雜度較高,所以ArrayList不適合進行隨機插入和刪除,而可以利用數組下標在O(1)時間內實現隨機訪問。

刪除元素

ArrayList支持兩種刪除方式:
1、按照下標刪除
2、按照元素刪除,這會刪除ArrayList中與指定要刪除的元素匹配的第一個元素
按照下標刪除,返回刪除的元素:

 /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
    //進行邊界檢查  看是否超過現在的容量
        rangeCheck(index);

        modCount++;    //爲迭代器拋出異常使用  其結構被改變的次數
        E oldValue = elementData(index);
        //要移動的元素的個數   
        int numMoved = size - index - 1;
        if (numMoved > 0)
        //將原數組的從index+1開始的numMoved個數拷貝到index開始的
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //將最後一個數設爲null。完成拷貝工作
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

按元素刪除操作,返回是否刪除成功:

//其實質也是通過遍歷尋找第一個此元素,完成刪除
    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;
    }

    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
    }

獲取/改變元素

因爲是利用動態數組實現的,所以隨機訪問肯定是其最大的優點:

//獲取index處的元素  
    public E get(int index) {
       //檢查是否在範圍內
        rangeCheck(index);

        return elementData(index);
    }
    //改變index處的元素值
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    //獲取某一元素的下標index
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

ArrayList總結

ArrayList的優缺點從上邊的源碼的分析就能明顯看到其優缺點:
1、ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快
2、ArrayList在順序添加一個元素的時候非常方便,只是往數組裏面添加了一個元素而已
不過ArrayList的缺點也十分明顯:
1、刪除元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費性能
2、插入元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費性能
因此,ArrayList比較適合順序添加、隨機訪問的場景。

ArrayList和Vector的區別

Vector底層也是通過動態數組實現的,也允許插入null。ArrayList是線程非安全的,這很明顯,因爲ArrayList中所有的方法都不是同步的,在併發下一定會出現線程安全問題。那麼我們想要使用ArrayList並且讓它線程安全怎麼辦?一個方法是用Collections.synchronizedList方法把你的ArrayList變成一個線程安全的List,比如:

List<String> synchronizedList = Collections.synchronizedList(list); 

最好的方法是使用併發包中的集合。另一個方法就是Vector,它是ArrayList的線程安全版本,其實現90%和ArrayList都完全一樣,區別在於:
1、Vector是線程安全的,ArrayList是線程非安全的
2、Vector可以指定增長因子,如果該增長因子指定了,那麼擴容的時候會每次新的數組大小會在原數組的大小基礎上加上增長因子;如果不指定增長因子,那麼就給原數組大小*2,源代碼是這樣的:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //擴容方法:如果指定了capacityIncrement 因子,就在原容量上+此因子
        //否則:就變爲原來的2倍   這和ArrayList是不同的
        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);
    }

另外:
看到ArrayList實現了Serializable接口,這意味着ArrayList是可以被序列化的,用transient修飾elementData意味着我不希望elementData數組被序列化。這是爲什麼?因爲序列化ArrayList的時候,ArrayList裏面的elementData未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那麼是否有必要序列化整個elementData呢?顯然沒有這個必要,因此ArrayList中重寫了writeObject方法:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        //ObjectOutputStream 的序列化對象方法  先序列化無transient修飾的
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        //然後自定義序列化  只序列化元素  size+元素值
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

每次序列化的時候調用這個方法,先調用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然後遍歷elementData,只序列化那些有的元素,這樣:
1、加快了序列化的速度
2、減小了序列化之後的文件大小

到這裏,我們就完成了ArrayList的源碼分析,下一篇,重點分析LinkedList的實現。

發佈了53 篇原創文章 · 獲贊 119 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章