Java -- 基於JDK1.8的ArrayList源碼分析

1,前言java

  好久沒有寫博客了,很想念你們,18年都快過完了,纔開始寫第一篇,爭取後面每週寫點,權當是記錄,由於最近在看JDK的Collection,並且ArrayList源碼這一塊也常常被面試官問道,因此今天也就和你們一塊兒來總結一下面試

2,源碼解讀api

  當咱們通常提到ArrayList的話都會脫口而出它的幾個特色:有序、可重複、查找速度快,可是插入和刪除比較慢,線程不安全,那麼如今阿呆哥哥就會有這些疑問:爲何說是有序的?怎麼有序?爲何又說插入和刪除比較慢?爲何慢?還有線程爲何不安全?因此帶着這些問題,咱們一一的來源碼中來找找答案。數組

  通常對於一個陌生的類,咱們想使用它,都會先看它構造方法,再看它的屬性和方法,那麼咱們也按照這種方式來讀讀ArrayList這個類安全

  2.1構造方法多線程

ArrayList<String> arrayList = new ArrayList();
ArrayList<String> arrayList1 = new ArrayList(2);

  通常來講咱們常見使用ArrayList的建立方式是上面的這兩種框架

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;

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


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

  上面是咱們兩個構造方法和咱們類中基本的屬性,從上面的代碼上來看,在建立構造基本上都沒有作,且定義了兩個默認的空數組,默認容器的大小DEFAULT_CAPACITY爲10,還有咱們真正存儲元素的地方elementData數組,因此這就是爲何說ArrayList存儲集合元素的底層時是使用數組來實現,OK,上面的代碼除了一個transient 修飾符以外咱們同窗們可能有點陌生以外,其他的應該都能看的懂,transient 有什麼做用還有爲何用它修飾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;
        }
}

  第2行:利用Collection.toArray()方法獲得一個對象數組,並賦值給elementData ui

  第3行:size表明集合的大小,當經過別的集合來構造ArrayList的時候,須要賦值size

  第5-6行:判斷 c.toArray()是否出錯返回的結果是否出錯,若是出錯了就利用Arrays.copyOf 來複制集合c中的元素到elementData數組中

  第9行:若是c中元素數量爲空,則將EMPTY_ELEMENTDATA空數組賦值給elementData

  上面就是全部的構造函數的代碼了,這裏咱們能夠看到,當構造函數走完以後,會建立出數組elementData和初始化size,Collection.toArray()則是將Collection中全部元素賦值到一個數組,Arrays.copyOf()則是根據Class類型來決定是new仍是反射來創造對象並放置到新的數組中,源碼以下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
}

  這裏面System.arraycopy(Object src,  int  srcPos, Object dest, int destPos,  int length) 這個方法在咱們的後面會的代碼中會出現,就先講了,定義是:將數組src從下標爲srcPos開始拷貝,一直拷貝length個元素到dest數組中,在dest數組中從destPos開始加入先的srcPos數組元素。至關於將src集合中的[srcPos,srcPos+length]這些元素添加到集合dest中去,起始位置爲destPos

  2.2 增長元素方法

  通常常用的是下面三種方法

arrayList.add( E element);
arrayList.add(int index, E element);
arrayList.addAll(Collection<? extends E> c);

  讓咱們一個個來看看

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        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++;

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

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

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 = 0x7fffffff;
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8

  第2行:調用ensureCapacityInternal()函數

  第8-9行:判斷當前是不是使用默認的構造函數初始化,若是是設置最小的容量爲默認容量10,即默認的elementData的大小爲10(這裏是有一個容器的概念,當前容器的大小通常是大於當前ArrayList的元素個數大小的)

  第16行:modCount字段是用來記錄修改過擴容的次數,調用ensureExplicitCapacity()方法意味着肯定修改容器的大小,即確認擴容

  第26-30、35-44行:通常默認是擴容1.5倍,當時當發現仍是不能知足的話,則使用size+1以後的元素個數,若是發現擴容以後的值大於咱們規定的最大值,則判斷size+1的值是否大於MAX_ARRAY_SIZE的值,大於則取值MAX_VALUE,反之則MAX_ARRAY_SIZE,也就數說容器最大的數量爲MAX_VALUE

  第32行:就是拷貝以前的數據,擴大數組,且構建出一個新的數組

  第3行:這時候數組擴容完畢,就是要將須要添加的元素加入到數組中了

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-3行:判斷插入的下標是否越界

  第5行:和上面的同樣,判斷是否擴容

  第6行:System.arraycopy這個方法的api在上面已經講過了,這裏的話則是將數組elementData從index開始的數據向後移動一位

  第8-9行:則是賦值index位置的數據,數組大小加一

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

  第2行:將集合轉成數組,這時候源碼沒有對c空很奇怪,若是傳入的Collection爲空就直接空指針了

  第3-7行:獲取數組a的長度,進行擴容判斷,再將新傳入的數組複製到elementData數組中去

  因此對增長數據的話主要調用add、addAll方法,判斷是否下標越界,是否須要擴容,擴容的原理是每次擴容1.5倍,若是不夠的話就是用size+1爲容器值,容器擴充後modCount的值對應修改一次

  2.3 刪除元素方法  

  經常使用刪除方法有如下三種,咱們一個個來看看

arrayList.remove(Object o);
arrayList.remove(int index)
arrayList.removeAll(Collection<?> c)

  

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
}

  從上面源碼能夠看出,若是要移除的元素爲null和不爲空,都是經過for循環找到要被移除元素的第一個下標,因此這裏咱們就會思考,當咱們的集合中有多個null的話,是否是調用remove(null)這個方法只會移除第一個出現的null元素呢?這個須要同窗們下去驗證一下。而後經過System.arraycopy函數,來從新組合elementData中的值,且elementData[size]置空原尾部數據 再也不強引用, 能夠GC掉。

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) 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;
}

  能夠看到remove(int index)更簡單了,都不須要經過for循環將要刪除的元素下邊確認下來,總體的邏輯和上面經過元素刪除的沒什麼區別,再來看看批量刪除

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
}

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
}

private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

  第二、6-10行:對傳入集合c進行判空處理

  第13-15行:定義局部變量elementData、r、w、modified   elementData用來從新指向成員變量elementData,用來存儲最終過濾後的元素,w用來紀錄過濾以後集合中元素的個數,modified用來返回此次是否有修改集合中的元素

  第17-19行:for循環遍歷原有的elementData數組,發現若是不是要移除的元素,則從新存儲在elementData,且w自增

  第23-28行:若是出現異常,則會致使 r !=size , 則將出現異常處後面的數據所有複製覆蓋到數組裏。

  第29-36行:判斷若是w!=size,則代表原先elementData數組中有元素被移除了,而後將數組尾端size-w個元素置空,等待gc回收。再修改modCount的值,在修改當前數組大小size的值

  2.3 修改元素方法

arrayList.set(int index, E element)

  常見的方法也就是上面這一種,咱們來看看它的實現的源碼

public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

  源碼很簡單,首先去判斷是否越界,若是沒有越界則將index下表的元素從新賦值element新值,將老值oldValue返回回去

  2.4 查詢元素方法

arrayList.get(int index);

  讓咱們看看源碼

public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
}

  源碼也炒雞簡單,首先去判斷是否越界,若是沒有越界則將index下的元素從elementData數組中取出返回

  2.5 清空元素方法

arrayList.clear();

  常見清空也就這一個方法

public void clear() {
        modCount++;

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

        size = 0;
}

  源碼也很簡單,for循環重置每個elementData數組爲空,修改size的值,修改modCount值

  2.6 判斷是否存在某個元素

arrayList.contains(Object o);
arrayList.lastIndexOf(Object o);

  常見的通常是contains方法,不過我這裏像把lastIndexOf方法一塊兒講了,源碼都差很少

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

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

public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

  經過上面的源碼,你們能夠看到,不論是contains方法仍是lastIndexOf方法,其實就是進行for循環,若是找到該元素則記錄下當前元素下標,若是沒找到則返回-1,很簡單

  2.7 遍歷ArrayList中的對象(迭代器)

Iterator<String> it = arrayList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
}

  咱們遍歷集合中的元素方法挺多的,這裏咱們就不講for循環遍歷,咱們來看看專屬於集合的iterator遍歷方法吧

public Iterator<E> iterator() {
        return new Itr();
}

private class Itr implements Iterator<E> {
        // Android-changed: Add "limit" field to detect end of iteration.
        // The "limit" of this iterator. This is the size of the list at the time the
        // iterator was created. Adding & removing elements will invalidate the iteration
        // anyway (and cause next() to throw) so saving this value will guarantee that the
        // value of hasNext() remains stable and won't flap between true and false when elements
        // are added and removed from the list.
        protected int limit = ArrayList.this.size;

        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor < limit;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            int i = cursor;
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
                limit--;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

  第1-3行:在獲取集合的迭代器的時候,去new了一個Itr對象,而Itr實現了Iterator接口,咱們主要重點關注Iterator接口的hasNext、next方法

  第12-16行:定義變量,limit:用來記錄當前集合的大小值;cursor:遊標,默認爲0,用來記錄下一個元素的下標;lastRet:上一次返回元素的下標

  第18-20行:判斷當前遊標cursor的值是否超過當前集合大小zise,若是沒有則說明後面還有元素

  第24-31行:在這裏面作了很多線程安全的判斷,在這裏若是咱們異步的操做了集合就會觸發這些異常,而後獲取到集合中存儲元素的elemenData數組

  第32-33行:遊標cursor+1,而後返回元素 ,並設置此次次返回的元素的下標賦值給lastRet

 

3,看源碼以前問題的反思

  ok,上面的話基本上把咱們ArrayList經常使用的方法的源碼給看完了。這時候,咱們須要來對以前的問題來一一進行總結了

  ①有序、可重複是什麼概念?

public static void main(String[] args){
        ArrayList arrayList = new ArrayList();
        arrayList.add("1");
        arrayList.add("1");
        arrayList.add("2");
        arrayList.add("3");
        arrayList.add("1");
        Iterator<String> it = arrayList.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
}

輸出結果
1
1
2
3
1

  可重複是指加入的元素能夠重複,有序是指的加入元素的順序和取出來的時候順序相同,通常這個特色是List相對於Set和Map來比較出來的,後面咱們把Set、Map的源碼看了以後會更加理解這兩個特色

  ② 爲何說查找查找元素比較快,但添加和刪除元素比較慢呢?

  咱們從上面的源碼獲得,當增長元素的時候是有可能會觸發擴容機制的,而擴容機制會致使數組複製;刪除和批量刪除會致使找出兩個集合的交集,以及數組複製操做;而查詢直接調用return (E) elementData[index]; 因此說增、刪都相對低效 而查找是很高效的操做。

  ③ 爲何說ArrayList線程是不安全

  從上面的代碼咱們都知道,如今add()方法爲例

public boolean add(E e) {
        //肯定是否擴容,這裏能夠忽略
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }    

  這裏咱們主要看兩點,第一點add()方法前面沒有synchronized字段、第二點 elementData[size++] = e;這段代碼能夠拆開爲下面兩部分代碼

 elementData[size] = e;
 size++

  也就是說整個add()方法能夠拆爲兩步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。咱們都知道咱們的CUP是切換進程運行的,在單線程中這樣是沒有問題的,可是通常在咱們項目中不少狀況是在多線程中使用ArrayList的,這時候好比有兩個線程,線程 A 先將元素存放在位置 0。可是此時 CPU 調度線程A暫停,線程 B 獲得運行的機會。線程B也向此 ArrayList 添加元素,由於此時 Size 仍然等於 0 ,因此線程B也將元素存放在位置0。而後線程A和線程B都繼續運行,都增長 Size 的值。這樣就會獲得元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這樣就形成了咱們的線程不安全了。

  你們能夠寫一個線程搞兩個線程來試試,看看size是否是有問題,這裏就不帶你們一塊兒寫了。

  ④ transient 關鍵字有什麼用?

  唉,這個就有點意思了,這個是咱們以前讀源碼讀出來的遺留問題,那源碼如今讀完了,是時候來解決這個問題了,咱們來看看transient官方給的解釋是什麼

當對象被序列化時(寫入字節序列到目標文件)時,transient阻止實例中那些用此關鍵字聲明的變量持久化;當對象被反序列化時(從源文件讀取字節序列進行重構),這樣的實例變量值不會被持久化和恢復。

  而後咱們看一下ArrayList的源碼中是實現了java.io.Serializable序列化了的,也就是transient Object[] elementData; 這行代碼的意思是不但願elementData被序列化,那這時候咱們就有一個疑問了,爲何elementData不進行序列化?這時候我去網上找了一下答案,以爲這個解釋是最合理且易懂的

在ArrayList中的elementData這個數組的長度是變長的,java在擴容的時候,有一個擴容因子,也就是說這個數組的長度是大於等於ArrayList的長度的,咱們不但願在序列化的時候將其中的空元素也序列化到磁盤中去,因此須要手動的序列化數組對象,因此使用了transient來禁止自動序列化這個數組

  這時候咱們是懂了爲何不給elementData進行序列化了,那當咱們要使用序列化對象的時候,elementData裏面的數據是否是不能使用了?這裏ArrayList的源碼提供了下面方法

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        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();
        }
    }

    /**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

    經過writeObject方法將數據非null數據寫入到對象流中,再使用readObject讀取數據

 

4,總結

  上面咱們寫了這麼一大篇,是時候該來總結總結一下了

  ①查詢高效、但增刪低效,增長元素若是致使擴容,則會修改modCount,刪出元素必定會修改。 改和查必定不會修改modCount。增長和刪除操做會致使元素複製,所以,增刪都相對低效。而在咱們常見的Android場景中,ArrayList多用於存儲列表的數據,列表滑動時須要展現每個Item(element)的數組,因此查詢操做是最高頻的,且增長操做只有在列表加載更多時纔會用到 ,並且是在列表尾部插入,因此也不須要移動數據的操做。而刪操做則更低頻。 故選用ArrayList做爲保存數據的結構

  ②線程不安全,這個特色通常會和Vector作比較,Vector的源碼,內部也是數組作的,區別在於Vector在API上都加了synchronized因此它是線程安全的,以及Vector擴容時,是翻倍size,而ArrayList是擴容50%。Vector的源碼你們能夠在後面閒下來的時候看看,這裏給你們留一個思考題:既然Vector是安全的,那爲何咱們在平常開發Android中基本上沒有用到Vector呢?你們能夠閒下來的時候來尋找一下這個問題的答案

  最後再囉嗦一句,寫徹底篇後發現 ,感受很久沒寫博客手很生了,在寫的過程總髮現大致框架不對也在一點點的修復,後面爭取堅持寫下來,加油!!!

相關文章
相關標籤/搜索