ArrayList原始碼分析 jdk1.8

俺就不起網名發表於2018-04-08

本篇分析ArrayList的原始碼,在分析之前先跟大家談一談陣列。陣列可能是我們最早接觸到的資料結構之一,它是在記憶體中劃分出一塊連續的地址空間用來進行元素的儲存,由於它直接操作記憶體,所以陣列的效能要比集合類更好一些,這是使用陣列的一大優勢。


但是我們知道陣列存在致命的缺陷,就是在初始化時必須指定陣列大小,並且在後續操作中不能再更改陣列的大小。在實際情況中我們遇到更多的是一開始並不知道要存放多少元素,而是希望容器能夠自動的擴充套件它自身的容量以便能夠存放更多的元素。


ArrayList就能夠很好的滿足這樣的需求,它能夠自動擴充套件大小以適應儲存元素的不斷增加。它的底層是基於陣列實現的,因此它具有陣列的一些特點,例如查詢修改快而插入刪除慢。本篇我們將深入原始碼看看它是怎樣對陣列進行封裝的。

1、類的繼承

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2、類的屬性

 // 版本號
    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;
    // 實際元素大小,預設為0
    private int size;
    // 最大陣列容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

說明:類的屬性中核心的屬性為elementData,型別為Object[],用於存放實際元素,並且被標記為transient,也就意味著在序列化的時候,此欄位是不會被序列化的。但是ArrayList類中重寫了readObject和writeObject方法,所以其實是有序列化的。

3、建構函式

3.1、無參建構函式

    public ArrayList() { 
        // 無參建構函式,設定元素陣列為空 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

說明:當未指定初始化大小時,會給elementData賦值為空集合。

3.2、ArrayList(int)型建構函式

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { // 初始容量大於0
            this.elementData = new Object[initialCapacity]; // 初始化元素陣列
        } else if (initialCapacity == 0) { // 初始容量為0
            this.elementData = EMPTY_ELEMENTDATA; // 為空物件陣列
        } else { // 初始容量小於0,丟擲異常
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        }
    }
3.3、集合引數建構函式
 public ArrayList(Collection<? extends E> c) { // 集合引數建構函式
        elementData = c.toArray(); // 轉化為陣列
        if ((size = elementData.length) != 0) { // 引數為非空集合
            if (elementData.getClass() != Object[].class) // 是否成功轉化為Object型別陣列
                elementData = Arrays.copyOf(elementData, size, Object[].class); // 不為Object陣列的話就進行復制
        } else { // 集合大小為空,則設定元素陣列為空
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

說明:當傳遞的引數為集合型別時,會把集合型別轉化為陣列型別,並賦值給elementData。(呼叫Arrays.copyOf集合複製方法)

總結:

可以看到ArrayList的內部儲存結構就是一個Object型別的陣列,因此它可以存放任意型別的元素。在構造ArrayList的時候,如果傳入初始大小那麼它將新建一個指定容量的Object陣列,如果不設定初始大小那麼它將不會分配記憶體空間而是使用空的物件陣列,在實際要放入元素時再進行記憶體分配。另外注意:初始化的引數大小和集合的size值是不一樣的值,一個是陣列的預設可存大小(或者說是陣列長度),一個是集合的實際多少個。

4、核心函式

在介紹add之前,屬性陣列複製的兩個方法:

方法一、 System.arraycopy:

 public static native void arraycopy(Object src,  int  srcPos,
                                     Object dest, int destPos,
                                     int length);
引數分別為:
 src:源陣列; srcPos:源陣列要複製的起始位置; 
 dest:目的陣列; destPos:目的陣列放置的起始位置;

 length:複製的長度。

方法二、Arrays.copyOf:

Arrays.copyOf(original,newLength);
其底層其實也呼叫的是System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));

4.1、add函式

public boolean add(E e) { // 新增元素
        ensureCapacityInternal(size + 1);  // 確認容量大小
        elementData[size++] = e; //將元素新增到末尾
        return true;
    }

在add函式我們發現還有其他的函式ensureCapacityInternal,此函式可以理解為確保elementData陣列有合適的大小。ensureCapacityInternal的具體函式如下:

    //確認容量大小
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判斷元素陣列是否為空陣列(構造時未賦初始容量,第一次add時才為true)
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取較大值
        }
        ensureExplicitCapacity(minCapacity);
    }

在ensureCapacityInternal函式中我們又發現了ensureExplicitCapacity函式,這個函式也是為了確保elemenData陣列有合適的大小。ensureExplicitCapacity的具體函式如下:

    //確認是否需要擴容
    private void ensureExplicitCapacity(int minCapacity) {
        // 結構性修改加1
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

在ensureExplicitCapacity函式我們又發現了grow函式,grow函式才會對陣列進行擴容,ensureCapacityInternal、ensureExplicitCapacity都只是過程,最後完成實際擴容操作還是得看grow函式,grow函式的具體函式如下:

 private void grow(int minCapacity) {
        int oldCapacity = elementData.length; // 舊容量
        int newCapacity = oldCapacity + (oldCapacity >> 1); //新陣列的容量, 在原來的基礎上增加一半
        if (newCapacity - minCapacity < 0) // 新容量小於引數指定容量,修改新容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0) // //檢驗新的容量是否超過最大陣列容量,超過則賦最大容量值給它
            newCapacity = hugeCapacity(minCapacity); // 指定新容量
        // 拷貝擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

說明:每次新增一個元素到集合中都會先檢查容量是否足夠,否則就進行擴容。正常情況下會擴容1.5倍,特殊情況下(新擴充套件陣列大小已經達到了最大值)則只取最大值。

總結:

每次新增元素前會呼叫ensureCapacityInternal這個方法進行集合容量檢查。在這個方法內部會檢查當前集合的內部陣列是否還是個空陣列,如果是就新建預設大小為10的Object陣列。如果不是則證明當前集合已經被初始化過,那麼就呼叫ensureExplicitCapacity方法檢查當前陣列的容量是否滿足這個最小所需容量,不滿足的話就呼叫grow方法進行擴容。在grow方法內部可以看到,每次擴容都是增加原來陣列長度的一半,擴容實際上是新建一個容量更大的陣列,將原先陣列的元素全部複製到新的陣列上,然後再拋棄原先的陣列轉而使用新的陣列。

add函式還有一種構造方法:

public void add(int index, E element) {
   //插入位置範圍檢查
   rangeCheckForAdd(index);
   //檢查是否需要擴容
   ensureCapacityInternal(size + 1);
   //挪動插入位置後面的元素
   System.arraycopy(elementData, index, elementData, index + 1, size - index);
   //在要插入的位置賦上新值
   elementData[index] = element;
   size++;
}

4.2、其他方法

//刪
public E remove(int index) {
   //index不能大於size
   rangeCheck(index);
   modCount++;
   E oldValue = elementData(index);
   int numMoved = size - index - 1;
   if (numMoved > 0) {
       //將index後面的元素向前挪動一位
       System.arraycopy(elementData, index+1, elementData, index, numMoved);
   }
   //置空引用
   elementData[--size] = null;
   return oldValue;
}

//改
public E set(int index, E element) {
   //index不能大於size
   rangeCheck(index);
   E oldValue = elementData(index);
   //替換成新元素
   elementData[index] = element;
   return oldValue;
}

//查
public E get(int index) {
   //index不能大於size
   rangeCheck(index);
   //返回指定位置元素
   return elementData(index);
}

總結:具體增刪改查要注意的地方

增(新增):僅是將這個元素新增到末尾。操作快速。

增(插入):由於需要移動插入位置後面的元素,並且涉及陣列的複製,所以操作較慢。

刪:由於需要將刪除位置後面的元素向前挪動,也會設計陣列複製,所以操作較慢。

改:直接對指定位置元素進行修改,不涉及元素挪動和陣列複製,操作快速。

查:直接返回指定下標的陣列元素,操作快速。

通過原始碼看到,由於查詢和修改直接定位到陣列下標,不涉及元素挪動和陣列複製所以較快,而插入刪除由於要挪動元素,涉及到陣列複製,操作較慢。並且每次新增操作還可能進行陣列擴容,也會影響到效能。

5、最後總結

5.1、ArrayList底層實現是基於陣列的,因此對指定下標的查詢和修改比較快,但是刪除和插入操作比較慢;
5.2、構造ArrayList時儘量指定容量,減少擴容時帶來的陣列複製操作;
5.3、每次新增元素之前會檢查是否需要擴容,每次擴容都是增加原有容量的一半;
5.4、每次對下標的操作都會進行安全性檢查,如果出現陣列越界就立即丟擲異常;
5.5、ArrayList的所有方法都沒有進行同步,因此它不是執行緒安全的。

轉載部落格:

jdk1.8版本ArrayList分析

jdk1.7版本ArrayList分析

相關文章