Java集合:ArrayList源碼分析

其實我看到已有很多大佬寫過此類文章,並且寫的也比較清晰明瞭,那我爲何要再寫一遍呢?其實也是爲了加深本身的印象,鞏固本身的基礎html

(主要是不少文章沒有寫出來我想知道的東西!!!​!!!!)java


前言 

我說一下我認爲怎麼樣才能去看懂,看透徹一個源碼。

在你去分析源碼的時候,首先要會用,要明白這個工具類的做用,若是連一個工具類都包含哪些功能,這些功能的做用都不清楚,我以爲看源碼就是一種煎熬。(固然,大佬除外)api

​自我洗腦中~我是大佬!我是大佬!我是大佬!(he~tui!我不配!!!)數組


正文

本次是基於JDK1.8來具體分析ArrayList源碼dom

ArrayList的概念:

動態數組,它提供了動態的增長和減小元素,實現了Collection和List接口,靈活的設置數組的大小等好處。

每一個 ArrayList 實例都有一個容量。該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加函數

一、繼承結構分析

咱們先來看一下ArrayList類的繼承結構:工具

Java支持單繼承,多實現源碼分析

AbstractList<E>性能

抽象接口類,目的是使用抽象類中已經實現的方法。

咱們點開AbstractList<E>源碼,會看到其實AbstractList<E>已經也實現了List<E>接口,爲何要先繼承AbstractList<E>,而讓AbstractList先實現List<E>?而不是讓ArrayList直接實現List<E>?優化

這裏是有一個思想,接口中全都是抽象的方法,而抽象類中能夠有抽象方法,還能夠有具體的實現方法,正是利用了這一點,讓AbstractList<E>實現接口中一些通用的方法,而如ArrayList就繼承這個AbstractList類,拿到一些通用的方法,而後本身在實現一些本身特有的方法,這樣一來,讓代碼更簡潔,就繼承結構最底層的類中通用的方法都抽取出來,先一塊兒實現了,減小重複代碼。因此通常看到一個類上面還有一個抽象類,應該就是這個做用。

List<E>

使用List的接口規範

RandomAccess

這個是一個標記性接口,經過查看api文檔,它的做用就是用來快速隨機存取,有關效率的問題,在實現了該接口的話,那麼使用普通的for循環來遍歷,性能更高,例如arrayList。而沒有實現該接口的話,使用Iterator來迭代,這樣性能更高,例如linkedList。因此這個標記性只是爲了讓咱們知道咱們用什麼樣的方式去獲取數據性能更好。

Cloneable

目的是使用clone方法。 想具體瞭解此方法的,點擊這裏,這篇文章寫的仍是不錯的Cloneable~~

Serializable

實現該序列化接口,代表該類能夠被序列化,什麼是序列化?簡單的說,就是可以從類變成字節流傳輸,而後還能從字節流變成原來的類。

🤔🤔🤔🤔爲何AbstractList已經實現了List,ArrayList還要再實現一次呢? 

  • 其實ArrayList再去實現一次List在這裏並無什麼實際意義,這實際上是一個錯誤,由於做者寫這代碼的時候以爲這個會有用處,可是其實並沒什麼用,但由於沒什麼影響,就一直留到了如今。有興趣的同窗能夠去研究一下。

二、類分析 

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, 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 = {};

    /**
     * 數組元素(實際操做的數組,新增,刪除等方法都是在此數組發生操做)
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 實際數組的大小
     */
    private int size;

    /**
     * 數組的最大容量
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

這裏分析幾個地方:

(1)爲何數組最大容量是Integer.MAX_VALUE - 8,而不是Integer.MAX_VALUE

其實源碼中給了備註:意思應該是有些虛擬機在數組中保留了一些頭信息。避免內存溢出!

/**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

(2)爲何定義了兩個空數組

首先定義空數組的根本緣由是 優化處理,若是一個應用中有不少這樣ArrayList空實例的話,就會有不少的空數組,無疑是爲了優化性能,全部ArrayList空實例都指向同一個空數組。二者都是用來減小空數組的建立,全部空ArrayList都共享空數組。二者的區別主要是用來起區分做用,針對有參無參的構造在擴容時作區分走不一樣的擴容邏輯,優化性能。

(3)elementData爲何定義成transient?

具體緣由看這裏: elementData定義爲transent的緣由

三、構造方法

Array List總共有三個構造方法,下面咱們一一分析

1)無參構造方法 ArrayList()

/**
     * 將空數組初始化大小爲10(將空數組初始化大小爲10,具體在何時初始化大小爲10,待會兒會說到)
     */
    public ArrayList() {
        // 將elementData元素數組初始化爲空數組
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

無參構造方法中,將元素數組elementData初始化爲空數組。(注意:這裏就體現了我上文說到的,爲何定義兩個空數組)

2)有參構造方法 ArrayList(int)

/**
     * 構造一個具備指定初始容量的列表
     *
     * @param  initialCapacity: 初始化數組的值
     */
    public ArrayList(int initialCapacity) {
        //若是初始化的值大於0,則給定elementData一個長度爲initialCapacity的數組
        if (initialCapacity > 0) { 
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) { // 若是初始化的值等於0,則初始化爲空數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else { //不然(小於0的狀況)拋出異常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

3)有參構造方法 ArrayList(Collection<? extends E> c)

/**
     * 構造一個指定元素的集合(此方法不太經常使用)
     * @param c 
     */
    public ArrayList(Collection<? extends E> c) {
        // 將集合轉換爲數組並賦值給elementData
        elementData = c.toArray();
        // 若是集合的大小不爲0
        if ((size = elementData.length) != 0) {
            // 若是轉換後的數組不是泛型(object),則須要用Arrays的工具轉換一下爲object數組(這裏再也不對Arrays.copyOf展開論述)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else { // 不然初始化elementData爲一個空數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

對於當前構造方法,我舉個例子,更清晰明瞭

四、經常使用方法源碼分析

boolean add(E e)

重中之重,ArrayList的核心奧祕!!!!

/**
     * 在數組中增長一個元素
     * @param e 元素對象
     */
    public boolean add(E e) {
        // 肯定內部容量是否夠用,size是元素數組中數據的個數,由於要添加一個元素,因此size+1,先判斷size+1的這個個數數組可否放得下,就在這個方法中去判斷是否數組.length是否夠用了。
        ensureCapacityInternal(size + 1); 
        // 將元素e賦值到elementData末尾
        elementData[size++] = e;
        return true;
    }

    // 此方法能夠理解爲中轉計算
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 判斷數組是否是空數組, 若是是空數組(此時minCapacity = 0 + 1 = 1),就將minCapacity初始化爲10,但此時僅僅是返回要初始化數組的大小,並無真正初始化數組爲10
        // private static final int DEFAULT_CAPACITY = 10;
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // Math.max(參數1,參數2)方法的意思是返回參數中最大的數,若是是空數組是,此時返回的是10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 若是初始化的集合不是空,則返回元素數組的size + 1
        return minCapacity;
    }
    
    // 別擔憂 我有奧妙全自動(奧妙洗衣粉~全國人民都知道~~)
    private void ensureExplicitCapacity(int minCapacity) {
        // 結構變化記錄+1 在父類AbstractList中定義了一個int型的屬性:modCount,記錄了ArrayList結構性變化的次數
        modCount++;
        
        // 判斷數組是否夠用,若是不夠用,則自動擴容
        // 一、當初始化的集合爲空數組時,此時minCapacity是10,而elementData的長度爲0,因此須要擴容
        // 二、當初始化的集合不爲空是,也就是給定了大小,或已經初始化了元素,此時的minCapacity = 實際數組個數+1,此時判斷集合不夠用,也須要進行擴容,不然元素會溢出
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    // 自動擴容
    private void grow(int minCapacity) {
        // oldCapacity:元素數組的實際長度(即擴充前的數組大小)
        int oldCapacity = elementData.length;
        // oldCapacity 擴容1.5倍賦值給newCapacity( >>爲右移運算符,至關於除以2 即oldCapacity/2 )
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 若是初始化爲空的狀況,則將數組擴容爲10,此時纔是真正初始化元素數組elementData大小爲10
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 若是1.5倍的數組大小超過了集合的最大長度,則調用hugeCapacity方法,從新計算,也就是給定最大的集合長度
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 已經肯定了大小,就將元素copy到elementData元素數組中~~
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        // 內存溢出判斷
        if (minCapacity < 0)
            throw new OutOfMemoryError();
        // 這裏的邏輯爲:若是須要擴容的大小比數組的最大值都大,就返回Integer,MAX_VALUE(int最大值),不然返回集合的最大值(int最大值-8)
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

void add(int index, E element)

增長元素到指定下標

/**
     * 增長元素到指定下標
     *
     * @param index 下標
     * @param element 元素
     */
    public void add(int index, E element) {
        // 參數校驗
        rangeCheckForAdd(index);
        // 此方法再也不贅述,參考上文Add方法重的論述
        ensureCapacityInternal(size + 1);
        // 請看下面代碼塊的註釋
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 將指定元素覆蓋到指定下標
        elementData[index] = element;
        // 長度size + 1
        size++;
    }

    /**
     * 適用於add 和 addAll的校驗方法
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

System.arraycopy 方法解析

/**
     * System提供了一個靜態方法arraycopy(),咱們可使用它來實現數組之間的複製
     * 函數爲:public static native void arraycopy(Object src,int srcPos,Object dest, int destPos,int length);
     * @param      src      the source array. 源數組
     * @param      srcPos   starting position in the source array. 源數組的起始位置
     * @param      dest     the destination array. 目標數組
     * @param      destPos  starting position in the destination data. 目標數組的起始位置
     * @param      length   the number of array elements to be copied. 複製的長度
         
    //舉個例子
    public static void main(String[] args){

        int[] arr = {1,2,3,4,5};
        int[] copied = new int[10];
        System.out.println(Arrays.toString(copied));

        System.arraycopy(arr, 0, copied, 1, 5);//5是複製的長度

        System.out.println(Arrays.toString(copied));

    }
   輸出結果爲:
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    [0, 1, 2, 3, 4, 5, 0, 0, 0, 0]

boolean remove(Object o)

根據元素進行刪除

public E remove(int index) {
        // 參數校驗
        rangeCheck(index);
        // 結構變化記錄+1
        modCount++;
        // 獲取舊數據,返回給開發人員,目的是讓開發人員知道刪除了哪一個數據
        E oldValue = elementData(index);
        // 計算須要元素須要移動的次數
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 同上文敘述
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 將最後一個元素置爲空(元素前移,最後一位置爲空),讓GC回收
        elementData[--size] = null;

        return oldValue;
    }

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

void clear()

清空集合

/**
     * 清空集合
     */
    public void clear() {
        modCount++;

        // 將數組置爲空,促使GC回收
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

E get(int index)

返回此列表中指定位置上的元素 

/**
     * 檢查給定的索引是否在範圍內。 若是沒有,則拋出一個適當的運行時異常。
     * @param index : 下標
     */
    public E get(int index) {
        // 校驗下標有效性
        rangeCheck(index);
        // 返回元素數組中指定index位置的數據
        return elementData(index);
    }
 
   
    private void rangeCheck(int index) {
        // 若是下標大於實際數組長度(元素數組最後一個數據下標爲size-1)
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

E set(int index, E element) 

/**
     * 覆蓋相應下標的數據
     * @param index 下標
     * @param element 元素數據
     */
    public E set(int index, E element) {
        // 校驗方法(再也不解釋,與get方法中同樣)
        rangeCheck(index);
        // 獲取到舊數據,這裏將舊數據返回出去,爲了讓開發者知道替換的是哪一個值
        E oldValue = elementData(index);
        // 將指定下標覆蓋爲新元素
        elementData[index] = element;
        return oldValue;
    }


結尾

其實ArrayList中還有不少不少方法,這裏就不在一一敘述了,由於你理解了本文中所說的源碼,其實其它再去理解,再去查看,是比較容易簡單的。

本篇文章就講到這裏,若是有寫的很差的地方,請多多指教,我是善良的小黑哥,但願與你們共同進步!!

相關文章
相關標籤/搜索