Java小白集合源碼的學習系列:ArrayList

ArrayList源碼學習

本文基於JDK1.8版本,對集合中的巨頭ArrayList作必定的源碼學習,將會參考大量資料,在文章後面都將會給出參考文章連接,本文用以鞏固學習知識。c++

ArrayList的繼承體系

ArrayList繼承了AbstracList這個抽象類,還實現了List接口,提供了添加、刪除、修改、遍歷等功能。至於其餘接口,之後再作總結。git

ArrayList核心源碼

底層基於數組實現,咱們能夠查看源碼,瞭解其擁有的一些屬性:github

private static final long serialVersionUID = 8683452581122892189L;

//默認的初始容量爲10
private static final int DEFAULT_CAPACITY = 10;

//若是指定數組容量爲0,返回該數組,至關於new ArrayList<>(0);
private static final Object[] EMPTY_ELEMENTDATA = {};

//沒有指定容量時,返回該數組,與上面不一樣的是:new ArrayList<>();
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//該數組保存着ArrayList存儲的元素,任何沒有指定容量的ArrayList在添加第一個元素後,將會擴容至初始容量10
transient Object[] elementData; // non-private to simplify nested class access

//表明了當前存儲元素的數量
private int size;

再次強調將EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA區分開來是爲了明確添加第一個元素時,應該擴容的大小,具體擴容的機制,後面會分析。數組

咱們再來瞧瞧它的構造器:安全

//該構造器用以建立一個能夠指定容量的列表
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //建立一個指定容量大小的數組
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //指定容量爲0,對應EMPTY_ELEMENTDATA數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    //默認無參構造器,賦值空數組,可是在第一次添加以後,容量變爲默認容量10
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    //傳入一個集合,根據該集合迭代器返回順序,構造一個指定集合裏元素的列表
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        //傳入集合不爲空長
        if ((size = elementData.length) != 0) {
            //傳入集合轉化爲的數組可能不是Object[]須要判斷
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //傳入集合爲元素數量爲0,用空數組代替便可
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    //指定集合爲null的話(並非說集合爲空長),調用ArrayList的toArray方法,可能會拋出空指針異常

ArrayList擴容機制

瞭解完ArrayList基本的屬性和構造器以後,咱們將對裏面包含的方法進行學習:ide

  • 上面說到,使用默認構造器時,初始化賦值實際上是個空數組,在添加了一個元素以後,容量纔會變成10,是否是會以爲有點好奇呢,咱們先來瞧一瞧它的add系列方法:
//沒有指定索引,默認在尾部添加元素
    public boolean add(E e) {
 
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //擴容以後,下一位賦值爲e,size加1
        elementData[size++] = e;
        return true;
    }

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

    //判斷是否爲默認構造器生成的數組,並將minCapacity置爲0;若是不是,minCapacity仍是傳入的size+1
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //使用默認構造器,那麼纔會返回所須要的最小容量爲默認容量10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //minCapacity = size+1 
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
        //定義在AbstractList中,用於存儲結構修改次數
        modCount++;

        //若是最小容量比數組總長度還大,就擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    //擴容操做
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //將舊容量右移一位在加上自己,像當於新容量爲就容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //1.新數組的容量仍是不能知足須要的最小容量,如初始指定容量爲0時的狀況
        //2.新數組越過了整數邊界,newCapacity將會小於0
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //若是新數組的容量比數組最大的容量Integer.MAX_VALUE - 8還大,
        //調用hugeCapacity方法
        if (newCapacity - MAX_ARRAY_SIZE > 0)

            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    //比較最小容量和MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //三目表達式:若是真的須要擴這麼大容量的狀況下:
        //1.最小容量大於MAX_ARRAY_SIZE,新容量等於Integer.MAX_VALUE,不然新容量爲Integer.MAX_VALUE-8
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
  • 根據擴容操做,若是咱們一開始使用的是默認構造器生成的數組,在第一次增長以後容量就會變成默認容量10,以後纔會以1.5倍進行擴容。
  • 可是若是咱們指定的是以0爲容量的話,會經過grow方法,前四次擴容每次都只是增長1,頻繁地調用copyOf就很是難受了,因此在知道目標大概多大時,能夠經過public void ensureCapacity(int minCapacity)方法預先設置容量。參考:https://www.iteye.com/topic/577602。(可是通過個人我的測試:在1億級或以上地數量上,沒有調用該方法要快一些,可是真實場景應該不會把這麼多的數據存放在裏面吧,因此能夠的話,用上這個方法,提高性能呀。)
  • 關於newCapacity - minCapacity < 0的思考,很容易能看出斷定條件是新容量<須要的最小容量。可是這個條件怎樣才能達到呢
    • 當原容量爲0或1時,擴容就會知足該條件。
    • 當原容量足夠大時,它的1.5倍會越過整數邊界,變爲負值,一樣知足。
  • 注意:移位運算效率會比整除運算更高一些性能

  • modCount表明的是已對列表進行結構更改的次數,能夠看到,每次執行添加操做時,必定都會讓該次數加1。設計到的fail-fast機制,咱們以後將會繼續學習,暫不贅述。
  • 其實擴容的方式就是咱們看到的,建立一個以新容量爲長度的新數組,並將原來數組的值所有拷貝到新數組上,最後讓elementData指向這個新數組。學習

文章寫到這裏,我大舒一口長氣,層層嵌套的調用終於結束了,不知道大家的心裏是否也和我同樣哈。咱們趁熱打鐵,趕忙看看另外一個重載的add方法。測試

//在索引爲index處插入E
    public void add(int index, E element) {
        //索引越界判斷
        rangeCheckForAdd(index);

        //同上,確保有足夠容量添加元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //實際上Arrays.copyOf的底層調用的就是這個方法,意思是在原數組上從索引的位置到最後總體向後複製一位,至關於移動的長度爲 (size-1) - index +1 = size -index
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //在將index處填上元素E
        elementData[index] = element;
        //元素數量+1
        size++;
    }

有了前面的鋪墊,相對來講就比較輕鬆了。咱們不妨看看判斷數組越界的方法,媽呀,這就更加清晰了,可是須要注意的是index==size在添加操做裏,至關於從尾部插入,並不會構成越界:

private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

看完了「增」的兩個方法,該輪到同是四胞胎兄弟的「刪」了。再談「刪」以前,咱們要明確,ArrayList底層基於數組實現可依靠連續索引值存取獲取數據就變得理所固然了:

E elementData(int index) {
    return (E) elementData[index];
}

下面是「刪」操做,須要注意的是,remove操做並不可以將容量減小,只是將其中的元素數量變少,自始至終只是size在變化,不信你看:

//移除指定位置的元素,並將其返回 
    public E remove(int index) {
        //範圍判斷
        rangeCheck(index);
        //操做列表,計數加1
        modCount++;
        //取出舊值
        E oldValue = elementData(index);

        //至關於把index+1位置向後的全部元素集體向前複製一位,複製的長度就是
        //(size-1)-(index+1)+1 = numMoved
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //執行集體拷貝動做
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //並讓最後一個空出來的位置指向null,點名讓GC清理
        elementData[--size] = null; 
        //返回舊值
        return oldValue;
    }

能夠稍微看一下rangeCheck的代碼,與add操做裏斷定略有不一樣,省去了index<0的判斷,我一開始很疑惑,後來發現後面有對數組的索引值取值,仍是會發生異常:

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

我以爲有必要總結一下System.arraycopy這個方法,public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);native修飾符,底層並非Java實現,而是c和c++。
這個方法的做用呢:就是從指定的源數組(src)從指定位置(srcPos)開始複製數組到目標數組(dest)的指定位置(destPos),複製的個數正好是length。
Arrays.copyOf這個方法雖然底層調用了System.arraycopy,可是使用上是不太同樣的,它不須要目標數組,系統會自動在內部新建一個數組,並返回。
哇,感受add部分講完,真的思路及其清晰,簡直豁然開朗呢。我們繼續來remove!

//移除指定元素,找到並刪除返回true,沒找到返回false
    public boolean remove(Object o) {
        //判斷指定的元素是否自己就是null值
        if (o == null) {
            for (int index = 0; index < size; index++)
                //找到同爲null值的那個「它」
                if (elementData[index] == null) {
                    //快速刪除,刪除操做和以前相似,只是省略了範圍判斷,就不贅述了
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                //不是空值的話,就找值相等的,注意不要elementData[index].equals(o),時刻避免空指針
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

還有一個範圍性的removeRange就不贅述了,總結一下:ArrayList中的remove操做基於數組的拷貝,並將remove的長度置空,元素數量相應減小(只是元素數量減小,數組容量並不會改變)。
對了,清理的話,clear方法會清理的相對乾淨一些,可是依舊只是size變化

public void clear() {
        modCount++;

        //將全部元素置空,等待GC寵幸
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

固然,若是你但願數組容量也發生變化的話。你能夠試試下面的這個方法:

//將ArrayList容量調整爲當前size的大小
    public void trimToSize() {
        modCount++;
        //基於三目運算
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

接下來,講一講至關簡單的setget這對基佬操做:

//用指定值替換隻當索引位置上的值
    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);
    }

而後是姐妹花操做:indexOflastIndexOf。(ps:尋找元素的過程能夠參考remove指定元素的過程),以indexOf爲例,lastIndexOf從尾部向前遍歷便可。

//判斷o在ArrayList中第一次出現的位置
    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;
    }

經過indexOf方法的返回值,咱們還能夠判斷某個元素是否存在:

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

除了單個元素增以外,ArrayList中還提供了能夠將整個集合增長到自己尾部的方法

//把傳入集合中的全部元素所有加到自己集合的後面,若是發生改變就返回true
    public boolean addAll(Collection<? extends E> c) {
        //將傳入集合轉化爲列表,若是傳入集合爲null,會發生空指針異常
        Object[] a = c.toArray();
        int numNew = a.length;
        //肯定新長度是否須要擴容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        //傳入爲空集合就爲false,由於不會發生改變
        return numNew != 0;
    }

它的重載方法是在指定位置插入另外一個集合中地全部元素,而且以迭代的順序排列

//在指定位置插入另外一集合中的全部元素
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        //仍是會引起空指針
        Object[] a = c.toArray();
        //傳入新集合c的元素個數
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //要移動的個數:(size-1)-index+1 = numMoved
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        //size<index的狀況,前面就會拋異常,因此這裏只能index==size,至關於從尾部添加
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

最後的總結

  • ArrayList基於數組實現,查詢便利,經過擴容機制實現動態增加。

  • 默認構造器生成的ArrayList初始化賦值實際上是空數組,增長第一個元素以後變爲10.
  • 擴容機制讓每次的新容量都是原容量的1.5倍,且基於右移運算
  • 增長和刪除的操做底層基於數組拷貝,底層都調用了arraycopy的方法。
  • 因爲複製拷貝,致使增刪的操做大多數狀況下的效率會下降,可是並非絕對的,若是一直在尾部插,尾部刪的話,仍是挺快的。
  • 對了,它是線程不安全的,這個之後學習的時候在作總結吧。

對了若是不出意外的話,以後會帶來LinkedList的源碼學習,若是以爲我有敘述錯誤的地方,或者我沒有說明白點地方,還望評論區批評指正,一塊兒學習交流,加油加油!
參考連接:
淺談ArrayList動態擴容
List集合就這麼簡單【源碼剖析】
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md

相關文章
相關標籤/搜索