1. 程式人生 > 程式設計 >?史上最全的Java容器集合之ArrayList(原始碼解讀)

?史上最全的Java容器集合之ArrayList(原始碼解讀)

前言

文字已收錄至我的GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習群,群聊號碼:549684836 鼓勵大家在技術的路上寫部落格

絮叨

前面2篇的基礎,大家還是好好學習一下,下面是連結
?史上最全的Java容器集合之入門
?史上最全的Java容器集合之基礎資料結構(手撕連結串列)
本來想直接將List這個父類下的所有之類,但是怕太長,我把我們真實開發中最最常用的ArrayList單獨拿出來講了,後面2個做一篇(順便提一下,如果是零基礎的不建議來,有過半年工作經驗的跟著我一起把這些過一遍的話,對你的幫助是非常大的)

一、ArrayList認識

概念

概念:ArrayList是一個其容量能夠動態增長的動態陣列。但是他又和陣列不一樣,下面會分析對比。它繼承了AbstractList,實現了List、RandomAccess,Cloneable,java.io.Serializable。

RandomAccess介面,被List實現之後,為List提供了隨機訪問功能,也就是通過下標獲取元素物件的功能。

實現了Cloneable,java.io.Serializable意味著可以被克隆和序列化。

最主要的還是看我化圈的部分

ArrayList的資料結構

分析一個類的時候,資料結構往往是它的靈魂所在,理解底層的資料結構其實就理解了該類的實現思路,具體的實現細節再具體分析。

  ArrayList的資料結構是:   

 說明:底層的資料結構就是陣列,陣列元素型別為Object型別,即可以存放所有型別資料。我們對ArrayList類的例項的所有的操作底層都是基於陣列的。

ArrayList原始碼分析

繼承結構和層次關係

我們看一下ArrayList的繼承結構:

- ArrayList extends AbstractList
- AbstractList extends AbstractCollection 
複製程式碼

所有類都繼承Object 所以ArrayList的繼承結構就是上圖這樣

思考 為什麼要先繼承AbstractList,而讓AbstractList先實現List?而不是讓ArrayList直接實現List?

這裡是有一個思想,介面中全都是抽象的方法,而抽象類中可以有抽象方法,還可以有具體的實現方法,正是利用了這一點,讓AbstractList是實現介面中一些通用的方法,而具體的類, 如ArrayList就繼承這個AbstractList類,拿到一些通用的方法,然後自己在實現一些自己特有的方法,這樣一來,讓程式碼更簡潔,就繼承結構最底層的類中通用的方法都抽取出來,先一起實現了,減少重複程式碼。所以一般看到一個類上面還有一個抽象類,應該就是這個作用

RandomAccess這個介面

我點進去原始碼發現他是一個空的介面,為啥是個空介面呢

RandomAccess介面是一個標誌介面(Marker)

ArrayList集合實現這個介面,就能支援快速隨機訪問

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list,T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list,key);
        else
            return Collections.iteratorBinarySearch(list,key);
    } 

複製程式碼

通過檢視原始碼,發現實現RandomAccess介面的List集合採用一般的for迴圈遍歷,而未實現這介面則採用迭代器。 ArrayList用for迴圈遍歷比iterator迭代器遍歷快,LinkedList用iterator迭代器遍歷比for迴圈遍歷快, 所以說,當我們在做專案時,應該考慮到List集合的不同子類採用不同的遍歷方式,能夠提高效能! 然而有人發出疑問了,那怎麼判斷出接收的List子類是ArrayList還是LinkedList呢? 這時就需要用instanceof來判斷List集合子類是否實現RandomAccess介面!

總結:RandomAccess介面這個空架子的存在,是為了能夠更好地判斷集合是否ArrayList或者LinkedList,從而能夠更好選擇更優的遍歷方式,提高效能

ArrayList 類中的屬性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>,RandomAccess,java.io.Serializable
{
    // 版本號
    private static final long serialVersionUID = 8683452581122892189L;
    // 預設容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空物件陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 預設空物件陣列
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素陣列  ArrayList中有擴容這麼一個概念,正因為它擴容,所以它能夠實現“動態”增長
    transient Object[] elementData;
    // 實際元素大小,預設為0
    private int size;
    // 最大陣列容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

複製程式碼

構造方法

無參構造

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
複製程式碼

此時ArrayList的size為空,但是elementData的length為1。第一次新增時,容量變成初始容量大小10(預設無參構造的容量就是10)

int引數構造

   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);
        }
    }
複製程式碼

這個比較簡單 進來判斷是否大於0,如果大於0 就建立一個傳進來大小的一個陣列, 如果為0就是空陣列

collection引數建構函式

 public ArrayList(Collection<? extends E> c) {
        // 指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排
        // 這裡主要做了兩步:1.把集合的元素copy到elementData中。2.更新size值。
        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;
        }
    }

複製程式碼

把集合的元素重新放到新的集合裡面,然後更新實際容量的大小

具體方法

add方法

  /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        // 擴容操作,稍後講解
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 將元素新增到陣列尾部
        elementData[size++] = e;
        return true;
    }

複製程式碼

第一步 擴容操作 第二步 往尾部新增一個元素 跟著往下看

 ensureCapacityInternal(xxx); 確定內部容量的方法   

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) { //看,判斷初始化的elementData是不是空的陣列,也就是沒有長度
    //因為如果是空的話,minCapacity=size+1;其實就是等於1,空的陣列沒有長度就存放不了,所以就將minCapacity變成10,也就是預設大小,但是帶這裡,還沒有真正的初始化這個elementData的大小。
            minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
        }
    //確認實際的容量,上面只是將minCapacity=10,這個方法就是真正的判斷elementData是否夠用
        ensureExplicitCapacity(minCapacity);
    }
複製程式碼

ensureExplicitCapacity(xxx);

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

        // overflow-conscious code
//minCapacity如果大於了實際elementData的長度,那麼就說明elementData陣列的長度不夠用,不夠用那麼就要增加elementData的length。這裡有的讀者就會模糊minCapacity到底是什麼呢,這裡給你們分析一下

/*第一種情況:由於elementData初始化時是空的陣列,那麼第一次add的時候,minCapacity=size+1;也就minCapacity=1,在上一個方法(確定內部容量ensureCapacityInternal)就會判斷出是空的陣列,就會給
&emsp;&emsp;將minCapacity=10,到這一步為止,還沒有改變elementData的大小。
&emsp;第二種情況:elementData不是空的陣列了,那麼在add的時候,minCapacity=size+1;也就是minCapacity代表著elementData中增加之後的實際資料個數,拿著它判斷elementData的length是否夠用,如果length
不夠用,那麼肯定要擴大容量,不然增加的這個元素就會溢位。
*/


        if (minCapacity - elementData.length > 0)
    //arrayList能自動擴充套件大小的關鍵方法就在這裡了
            grow(minCapacity);
    }
複製程式碼

grow(xxx); arrayList核心的方法,能擴充套件陣列大小的真正祕密。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //將擴充前的elementData大小給oldCapacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
        if (newCapacity - minCapacity < 0)//這句話就是適應於elementData就空陣列的時候,length=0,那麼oldCapacity=0,newCapacity=0,所以這個判斷成立,在這裡就是真正的初始化elementData的大小了,就是為10.前面的工作都是準備工作。
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超過了最大的容量限制,就呼叫hugeCapacity,也就是將能給的最大值給newCapacity
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size,so this is a win:
    //新的容量大小已經確定好了,就copy陣列,改變容量大小咯。
        elementData = Arrays.copyOf(elementData,newCapacity);
    }
複製程式碼

 hugeCapacity();

//這個就是上面用到的方法,很簡單,就是用來賦最大值。
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
//如果minCapacity都大於MAX_ARRAY_SIZE,那麼就Integer.MAX_VALUE返回,反之將MAX_ARRAY_SIZE返回。因為maxCapacity是三倍的minCapacity,可能擴充的太大了,就用minCapacity來判斷了。
//Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639  也就是說最大也就能給到第一個數值。還是超過了這個限制,就要溢位了。相當於arraylist給了兩層防護。
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
複製程式碼

總結一下擴容過程

  • 1.判斷是否需要擴容,如果需要,計算需要擴容的最小容量
  • 2.如果確定擴容,就執行grow(int minCapacity),minCapacity為最少需要的容量
  • 3.第一次擴容是的容量大小是原來的1.5倍
  • 4如果第一次 擴容後容量還是小於minCapacity,那就擴容為minCapacity
  • 5.最後,如果minCapacity大於最大容量,則就擴容為最大容量

add(int,E)方法

public void add(int index,E element) {
        // 插入陣列位置檢查
        rangeCheckForAdd(index);

        // 確保容量,如果需要擴容的話則自動擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData,index,elementData,index + 1,size - index); // 將index後面的元素往後移一個位置
        elementData[index] = element; // 在想要插入的位置插入元素
        size++; // 元素大小加1
    }

  // 針對插入陣列的位置,進行越界檢查,不通過丟擲異常
  // 必須在0-最後一個元素中間的位置插入,,否則就報錯
  // 因為陣列是連續的空間,不存在斷開的情況
  private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

複製程式碼

這個也不難 前面我們陣列已經實現了

get方法

public E get(int index) {
        // 先進行越界檢查
        rangeCheck(index);
        // 通過檢查則返回結果資料,否則就丟擲異常。
        return elementData(index);
    }

    // 越界檢查的程式碼很簡單,就是判斷想要的索引有沒有超過當前陣列的最大容量
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    
        @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

複製程式碼

先檢查陣列越界,然後你就是直接去陣列那邊拿資料然後返回

set(int index,E element)

 // 作用:替換指定索引的元素
  public E set(int index,E element) {
        // 索引越界檢查
        rangeCheck(index);

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

複製程式碼

這個就是替換指定位置的元素

remove(int):通過刪除指定位置上的元素

public E remove(int index) {
       // 索引越界檢查
        rangeCheck(index);

        modCount++;
        // 得到刪除位置元素值
        E oldValue = elementData(index);

        // 計算刪除元素後,元素右邊需要向左移動的元素個數
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 進行移動操作,圖形過程大致類似於上面的add(int,E)
            System.arraycopy(elementData,index+1,numMoved);
        // 元素大小減1,並且將最後一個置為null.
        // 置為null的原因,就是讓gc起作用,所以需要顯示置為null
        elementData[--size] = null; // clear to let GC do its work

        // 返回刪除的元素值
        return oldValue;
    }

複製程式碼

remove(Object o)

public boolean remove(Object o) {
        // 如果刪除元素為null,則迴圈找到第一個null,並進行刪除
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    // 根據下標刪除
                    fastRemove(index);
                    return true;
                }
        } else {
        // 否則就找到陣列中和o相等的元素,返回下標進行刪除
            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,numMoved);
        // 元素大小減1,並且將最後一個置為null. 原因同上。
        elementData[--size] = null; // clear to let GC do its work
    }

複製程式碼

總結

ArrayList的優缺點

  • ArrayList底層以陣列實現,是一種隨機訪問模式,再加上它實現了RandomAccess介面,因此查詢也就是get的時候非常快
  • ArrayList在順序新增一個元素的時候非常方便,只是往陣列裡面添加了一個元素而已
  • 刪除元素時,涉及到元素複製,如果要複製的元素很多,那麼就會比較耗費效能
  • 插入元素時,涉及到元素複製,如果要複製的元素很多,那麼就會比較耗費效能

為什麼ArrayList的elementData是用transient修飾的

  • 說明: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;
        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();
        }
    }

複製程式碼
  • 優點:這樣做既提高了序列化的效率,減少了無意義的序列化;而且還減少了序列化後檔案大小。

版本說明

  • 這裡的原始碼是JDK8版本,不同版本可能會有所差異,但是基本原理都是一樣的。

結尾

ArrayList就這麼多了,大家自己可以對著部落格,對著原始碼看,我感激它這個原始碼不是很難,基於陣列的把可能是 因為博主也是一個開發萌新 我也是一邊學一邊寫 我有個目標就是一週 二到三篇 希望能堅持個一年吧 希望各位大佬多提意見,讓我多學習,一起進步。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇部落格有任何錯誤,請批評指教,不勝感激 !