ArrayList 源碼分析

ArrayList 源碼分析

1. 結構

  首先咱們須要對 ArrayList 有一個大體的瞭解就從結構來看看吧.java

1. 繼承

  該類繼承自 AbstractList 這個比較好說編程

2. 實現

這個類實現的接口比較多,具體以下:數組

  1. 首先這個類是一個 List 天然有 List 接口
  2. 而後因爲這個類須要進行隨機訪問,所謂隨機訪問就是用下標任一訪問,因此實現了RandomAccess
  3. 而後就是兩個集合框架確定會實現的兩個接口 Cloneable, Serializable 前面這個好說序列化一會咱們具體再說說

<!--more-->安全

3. 主要字段

// 默認大小爲10
    private static final int DEFAULT_CAPACITY = 10;
    // 空數組  
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默認的空數組  這個是在傳入無參的是構造函數會調用的待會再 add 方法中會看到
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 用來存放 ArrayList 中的元素 注意他的修飾符是一個 transient 也就是不會自動序列化
    transient Object[] elementData; 
    // 大小
    private int size;

4. 主要方法

下面的方法後面標有數字的就是表示重載方法數據結構

  1. ctor-3
  2. get
  3. set
  4. add-2
  5. remove-2
  6. clear
  7. addAll
  8. write/readObject
  9. fast-fail 機制
  10. subList
  11. iterator
  12. forEach
  13. sort
  14. removeIf

2. 構造方法分析

1. 無參的構造方法

   裏面只有一個操做就是把 elementData 設置爲 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個空數組。框架

// 無參的構造函數,傳入一個空數組  這時候會建立一個大小爲10的數組,具體操做在 add 中
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2. 傳入數組大小的構造

   這個就是 new 一個數組,若是數組大小爲0就 賦值爲 EMPTY_ELEMENTDATAdom

// 按傳入的參數建立新的底層數組
    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);
        }
    }

3. 傳入 Collection 接口

   在這個方法裏面主要就是把這個 Collection 轉成一個數組,而後把這個數組 copy 一下,若是這個接口的 size 爲0 和上面那個方法同樣傳入 EMPTY_ELEMENTDATAide

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            // 上面的註釋的意思是說 jdk 有一個 bug 具體來講就是一個 Object 類型的數組不必定可以存放 Object類型的對象,有可能拋異常
            // 主要是由於 Object 類型的數組可能指向的是他的子類的數組,存 Object 類型的東西會報錯
            if (elementData.getClass() != Object[].class)
                // 這個操做是首先new 了新的數組,而後再調用 System.arraycopy 拷貝值。也就是產生新的數組
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 傳入的是空的就直接使用空數組初始化
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

   可是注意一點這裏有一個 jdk 的 bug 也就是一個 Object 類型的數組不必定可以存放 Object類型的對象,有可能拋異常,主要是由於 Object 類型的數組可能指向的是他的子類的數組,存 Object 類型的東西會報錯。 爲了測試這個 bug 寫了幾行代碼測試一下。這個測試是通不過的,就是存在上面的緣由。函數式編程

   一個典型的例子就是 咱們建立一個 string 類型的 list 而後調用 toArray 方法發現返回的是一個 string[] 這時候天然就不能隨便存放元素了。函數

class A{
}

class B extends A {
}

public class JDKBug {

    @Test
    public void test1() {
        B[] arrB = new B[10];
        A[] arrA = arrB;
        arrA[0]=new A();
    }
}

3. 修改方法分析

1. Set 方法

   這個方法也很簡單 ,首先進行範圍判斷,而後就是直接更新下標便可。

// 也沒啥好說的就是,設置新值返回老值
    public E set(int index, E element) {
        rangeCheck(index);

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

2. Add(E e) 方法

  這個方法首先調用了 ensureCapacityInternal() 這個方法裏面就判斷了當前的 elementData 是否等於 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 若是是的話,就把數組的大小設置爲 10 而後進行擴容操做,這裏恰好解釋了爲何採用無參構造的List 的大小是 10 ,這裏擴容操做調用的方法是 ensureExplicitCapacity 裏面就幹了一件事若是用戶指定的大小 大於當前長度就擴容,擴容的方法採用了 Arrays.copy 方法,這個方法實現原理是 new 出一個新的數組,而後調用 System.arraycopy 拷貝數組,最後返回新的數組。

public boolean add(E e) {
        // 當調用了無參構造,設置大小爲10
        ensureCapacityInternal(size + 1);  // Increments modCount        
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        // 若是當前數組是默認空數組就設置爲 10和 size+1中的最小值
        // 這也就是說爲何說無參構造 new 的數組大小是 10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        // 若用戶指定的最小容量 > 最小擴充容量,則以用戶指定的爲準,不然仍是 10
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 1.5倍增加
        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);
    }

3. Add(int index, E e) 方法

   這個方法比較簡單和上面基本同樣,而後只是最後放元素的時候的操做不同,他是採用了 System.arraycopy 從本身向本身拷貝,目的就在於覆蓋元素。 注意一個規律這裏面只要涉及下標的操做的不少不是本身手寫 for 循環而是採用相似的拷貝覆蓋的方法。算是一個小技巧。

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

4. remove(int index)

  同理這裏面仍是用了拷貝覆蓋的技巧。 可是有一點注意的就是不用的節點須要手動的觸發 gc ,這也是在 Efftive Java 中做者舉的一個例子。

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

5. remove(E e)

   這個方法操做很顯然會判斷 e 是否是 null 若是是 null 的話直接採用 == 比較,不然的話就直接調用 equals 方法而後執行拷貝覆蓋。

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++)
                // 調用 equals 方法
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

6. clear()

   這個方法就幹了一件事,把數組中的引用全都設置爲 null 以便 gc 。而不是僅僅把 size 設置爲 0 。

// gc 全部節點
    public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }

7. addAll(Collection e)

   這個沒啥好說的就是,採用轉數組而後 copy

// 一個套路 只要涉及到 Collection接口的方法都是把這個接口轉成一個數組而後對數組操做
    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;
    }

4. 訪問方法分析

1. get

   直接訪問數組下標。

// 沒啥好說的直接去找數組下標
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

2. subList

   這個方法的實現比較有意思,他不是直接截取一個新的 List 返回,而是在這個類的內部還有一個 subList 的內部類,而後這個類就記錄了 subList 的開始結束下標,而後返回的是這個 subList 對象。你可能會想返回的 subList 他不是 List 不會有問題嗎,這裏這個 subList 是繼承的 AbstractList 因此仍是正確的。

public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
    }
    // subList 返回的是一個位置標記實例,就是在原來的數組上放了一些標誌,沒有修改或者拷貝新的空間
private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;
        // other functions .....
     }

5. 其餘功能方法

1. write/readObject

  前面在介紹數據域的時候我就有標註 elementData 是一個 transition 的變量也就是在自動序列化的時候會忽略這個字段。

   而後咱們又在源碼中找到到了 write/readObject 方法,這兩個方法是用來序列化 elementData 中的每個元素,也就是手動的對這個字段進行序列化和反序列化。這不是畫蛇添足嗎?

   既然要將ArrayList的字段序列化(即將elementData序列化),那爲何又要用transient修飾elementData呢?

   回想ArrayList的自動擴容機制,elementData數組至關於容器,當容器不足時就會再擴充容量,可是容器的容量每每都是大於或者等於ArrayList所存元素的個數。

   好比,如今實際有了8個元素,那麼elementData數組的容量多是8x1.5=12,若是直接序列化elementData數組,那麼就會浪費4個元素的空間,特別是當元素個數很是多時,這種浪費是很是不合算的。

   因此ArrayList的設計者將elementData設計爲transient,而後在writeObject方法中手動將其序列化,而且只序列化了實際存儲的那些元素,而不是整個數組。

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

2. fast-fail

   所謂的 fast-fail 就是在咱們進行 iterator 遍歷的時候不容許調用 Collection 接口的方法進行對容器修改,不然就會拋異常。這個實現的機制是在 iterator 中維護了兩個變量,分別是 modCountexpectedModCount 因爲 Collection 接口的方法在每次修改操做的時候都會對 modCount++ 因此若是在 iterator 中檢測到他們不相等的時候就拋異常。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
   }

3. forEach

   這個是一個函數式編程的方法,看看他的參數 forEach(Consumer<? super E> action) 頗有意思裏面接受是一個函數式的接口,咱們裏面回調了 Consumeraccept 因此咱們只須要傳入一個函數接口就能對每個元素處理。

@Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            //回調
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

寫了一段測試代碼,可是這個方法不經常使用,主要是 Collection 是能夠本身生成 Stream 對象,而後調用上面的方法便可。這裏提一下。

public class ArrayListTest {

    @Test
    public void foreach() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(1);
        list.add(4);
        list.add(6);
        list.forEach(System.out::print);  //打印每一次元素。
    }
}

4. sort

底層調用了 Arrays.sort 方法沒什麼好說的。

public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

5. removeIf

   這個和 forEach 差很少,就是回調寫好了。

6. Vector

以上基本是把 ArrayList 的重要的方法和屬性介紹完了,咱們已經比較清楚他底層的實現和數據結構了。而後提到 ArrayList 天然也少不了一個比較古老的容器 Vector 這個容器真的和 ArrayList 太像了。由於你會發現他們連繼承和實現的接口都是同樣的。可是也會有一些不一樣的地方,下面分條介紹一下。

  1. Vector 中基本全部的方法都是 synchronized 的方法,因此說他是線程安全的 ArrayList

  2. 構造方法不同,在屬性中沒有兩個比較特殊的常量,因此說他的構造方法直接初始化一個容量爲 10 的數組。而後他有四個構造方法。

  3. 遍歷的接口不同。他仍是有 iterator 的可是他之前的遍歷的方法是 Enumeration 接口,經過 elements 獲取 Enumeration 而後使用 hasMoreElementsnextElement 獲取元素。

  4. 缺乏一些函數式編程的方法。

相關文章
相關標籤/搜索