「必知必會」最細致的 ArrayList 原理分析


  從今天開始也正式開 JDK 原理分析的坑了,其實寫源碼分析的目的不再是像以前一樣搞懂原理,更重要的是看看他們編碼風格更進一步體會到他們的設計思想。看源碼前先自己實現一個再比對也許會有不一樣的收獲!

1. 結構

  首先我們需要對 ArrayList 有一個大致的了解就從結構來看看吧.

1. 繼承

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

2. 實現

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

  1. 首先這個類是一個 List 自然有 List 接口
  2. 然后由於這個類需要進行隨機訪問,所謂隨機訪問就是用下標任一訪問,所以實現了RandomAccess
  3. 然后就是兩個集合框架肯定會實現的兩個接口 Cloneable, Serializable 前面這個好說序列化一會我們具體再說說

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

3. 傳入 Collection 接口

   在這個方法里面主要就是把這個 Collection 轉成一個數組,然后把這個數組 copy 一下,如果這個接口的 size 為0 和上面那個方法一樣傳入 EMPTY_ELEMENTDATA

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. 缺少一些函數式編程的方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM