ArrayList源碼分析(擴容機制jdk8)

ArrayList概述

(1)ArrayList 是一種變長的集合類,基於定長數組實現。java

(2)ArrayList 容許空值和重複元素,當往 ArrayList 中添加的元素數量大於其底層數組容量時,其會經過擴容機制從新生成一個更大的數組。數組

(3)因爲 ArrayList 底層基於數組實現,因此其能夠保證在 O(1) 複雜度下完成隨機查找操做。安全

(4)ArrayList 是非線程安全類,併發環境下,多個線程同時操做 ArrayList,會引起不可預知的異常或錯誤。多線程

ArrayList的成員屬性

在介紹關於ArrayList的各類方法以前先看一下基礎屬性成員。其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA與EMPTY_ELEMENTDATA的區別是:當咱們向數組中添加第一個元素時,DEFAULTCAPACITY_EMPTY_ELEMENTDATA將會知道數組該擴充多少併發

//默認初始化容量
private static final int DEFAULT_CAPACITY = 10;

//默認的空的數組,這個主要是在構造方法初始化一個空數組的時候使用
private static final Object[] EMPTY_ELEMENTDATA = {};

//使用默認size大小的空數組實例,和EMPTY_ELEMENTDATA區分開來,
//這樣能夠知道當第一個元素添加的時候進行擴容至多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//ArrayList底層存儲數據就是經過數組的形式,ArrayList長度就是數組的長度。
//一個空的實例elementData爲上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當添加第一個元素的時候
//會進行擴容,擴容大小就是上面的默認容量DEFAULT_CAPACITY
transient Object[] elementData; // non-private to simplify nested class access

//arrayList的大小
private int size;
複製代碼

static修飾的EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA性能

ArrayList構造方法

(1)帶有初始化容量的構造方法this

  • 參數大於0,elementData初始化爲initialCapacity大小的數組
  • 參數小於0,elementData初始化爲空數組
  • 參數小於0,拋出異常
//參數爲初始化容量
public ArrayList(int initialCapacity) {
    //判斷容量的合法性
    if (initialCapacity > 0) {
        //elementData纔是實際存放元素的數組
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //若是傳遞的長度爲0,就是直接使用本身已經定義的成員變量(一個空數組)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
複製代碼

(2)無參構造spa

  • 構造方法中將elementData初始化爲空數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 當調用add方法添加第一個元素的時候,會進行擴容
  • 擴容至大小爲DEFAULT_CAPACITY=10
//無參構造,使用默認的size爲10的空數組,在構造方法中沒有對數組長度進行設置,會在後續調用add方法的時候進行擴容
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼

(3)參數爲Collection類型的構造器線程

//將一個參數爲Collection的集合轉變爲ArrayList(實際上就是將集合中的元素換爲了數組的形式)。若是
//傳入的集合爲null會拋出空指針異常(調用c.toArray()方法的時候)
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        //c.toArray()可能不會正確地返回一個 Object[]數組,那麼使用Arrays.copyOf()方法
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //若是集合轉換爲數組以後數組長度爲0,就直接使用本身的空成員變量初始化elementData
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
複製代碼

​ 上面的這些構造方法理解起來比較簡單,關注前兩個構造方法作的事情,目的都是初始化底層數組 elementData(this.elementData=XXX)。區別在於無參構造方法會將 elementData 初始化一個空數組,插入元素時,擴容將會按默認值從新初始化數組。而有參的構造方法則會將 elementData 初始化爲參數值大小(>= 0)的數組。通常狀況下,咱們用默認的構造方法便可。假若在可知道將會向 ArrayList 插入多少元素的狀況下,可使用有參構造方法。設計

​ 上面說到了使用無參構造的時候,在調用add方法的時候會進行擴容,因此下面咱們就看看add方法以及擴容的細節

ArrayList的add方法

add方法大體流程

//將指定元素添加到list的末尾
public boolean add(E e) {
    //由於要添加元素,因此添加以後可能致使容量不夠,因此須要在添加以前進行判斷(擴容)
    ensureCapacityInternal(size + 1);  // Increments modCount!!(待會會介紹到fast-fail)
    elementData[size++] = e;
    return true;
}
複製代碼

咱們看到add方法中在添加元素以前,會先判斷size的大小,因此咱們來看看ensureCapacityInternal方法的細節

ensureCapacityInternal方法分析

private void ensureCapacityInternal(int minCapacity) {
    //這裏就是判斷elementData數組是否是爲空數組
    //(使用的無參構造的時候,elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    //若是是,那麼比較size+1(第一次調用add的時候size+1=1)和DEFAULT_CAPACITY,
    //那麼顯然容量爲10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
複製代碼

​ **當 要 add 進第1個元素時,minCapacity爲(size+1=0+1=)1,在Math.max()方法比較後,minCapacity 爲10。**而後緊接着調用ensureExplicitCapacity更新modCount的值,並判斷是否須要擴容

ensureExplicitCapacity方法分析

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; //這裏就是add方法中註釋的Increments modCount
    //溢出
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);//這裏就是執行擴容的方法
}
複製代碼

​ 下面來看一下擴容的主要方法grow。

grow方法分析

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
    // oldCapacity爲舊數組的容量
    int oldCapacity = elementData.length;
    // newCapacity爲新數組的容量(oldCap+oldCap/2:即更新爲舊容量的1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 檢查新容量的大小是否小於最小須要容量,若是小於那舊將最小容量最爲數組的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //若是新容量大於MAX_ARRAY_SIZE,使用hugeCapacity比較兩者
    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);
}
複製代碼

hugeCapacity方法

這裏簡單看一下hugeCapacity方法

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //對minCapacity和MAX_ARRAY_SIZE進行比較
    //若minCapacity大,將Integer.MAX_VALUE做爲新數組的大小
    //若MAX_ARRAY_SIZE大,將MAX_ARRAY_SIZE做爲新數組的大小
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
複製代碼

add方法執行流程總結

​ 咱們用一幅圖來簡單梳理一下,當使用無參構造的時候,在第一次調用add方法以後的執行流程

​ 這是第一次調用add方法的過程,當擴容值capacity爲10以後,

  • 繼續添加第2個元素(先注意調用ensureCapacityInternal方法傳遞的參數爲size+1=1+1=2)

  • 在ensureCapacityInternal方法中,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA不成立,因此直接執行ensureExplicitCapacity方法

  • ensureExplicitCapacity方法中minCapacity爲剛剛傳遞的2,因此第二個if判斷(2-10=-8)不會成立,即newCapacity 不比 MAX_ARRAY_SIZE大,則不會進入 grow 方法。數組容量爲10,add方法中 return true,size增爲1。

  • 假設又添加三、4......10個元素(其中過程相似,可是不會執行grow擴容方法)

  • 當add第11個元素時候,會進入grow方法時,計算newCapacity爲15,比minCapacity(爲10+1=11)大,第一個if判斷不成立。新容量沒有大於數組最大size,不會進入hugeCapacity方法。數組容量擴爲15,add方法中return true,size增爲11。

add(int index,E element)方法

//在元素序列 index 位置處插入
public void add(int index, E element) {
    rangeCheckForAdd(index); //校驗傳遞的index參數是否是合法
    // 1. 檢測是否須要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 將 index 及其以後的全部元素都向後移一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 3. 將新元素插入至 index 處
    elementData[index] = element;
    size++;
}
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0) //這裏判斷的index>size(保證數組的連續性),index小於0
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製代碼

add(int index, E element)方法(在元素序列指定位置(假設該位置合理)插入)的過程大概是下面這些

  1. 檢測數組是否有足夠的空間(這裏的實現和上面的)
  2. 將 index 及其以後的全部元素向後移一位
  3. 將新元素插入至 index 處.

將新元素插入至序列指定位置,須要先將該位置及其以後的元素都向後移動一位,爲新元素騰出位置。這個操做的時間複雜度爲O(N),頻繁移動元素可能會致使效率問題,特別是集合中元素數量較多時。在平常開發中,若非所需,咱們應當儘可能避免在大集合中調用第二個插入方法。

ArrayList的remove方法

ArrayList支持兩種刪除元素的方式

一、remove(int index) 按照下標刪除

public E remove(int index) {
    rangeCheck(index); //校驗下標是否合法(若是index>size,舊拋出IndexOutOfBoundsException異常)
    modCount++;//修改list結構,就須要更新這個值
    E oldValue = elementData(index); //直接在數組中查找這個值

    int numMoved = size - index - 1;//這裏計算所須要移動的數目
    //若是這個值大於0 說明後續有元素須要左移(size=index+1)
    //若是是0說明被移除的對象就是最後一位元素(不須要移動別的元素)
    if (numMoved > 0)
        //索引index只有的全部元素左移一位 覆蓋掉index位置上的元素
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //移動以後,原數組中size位置null
    elementData[--size] = null; // clear to let GC do its work
    //返回舊值
    return oldValue;
}
//src:源數組 
//srcPos:從源數組的srcPos位置處開始移動
//dest:目標數組
//desPos:源數組的srcPos位置處開始移動的元素,這些元素從目標數組的desPos處開始填充
//length:移動源數組的長度
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
複製代碼

​ 刪除過程以下圖所示

二、remove(Object o) 按照元素刪除,會刪除和參數匹配的第一個元素

public boolean remove(Object o) {
    //若是元素是null 遍歷數組移除第一個null
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //遍歷找到第一個null元素的下標 調用下標移除元素的方法
                fastRemove(index);
                return true;
            }
    } else {
        //找到元素對應的下標 調用下標移除元素的方法
        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, index+1, elementData, index,
                     numMoved);
  elementData[--size] = null; // clear to let GC do its work
}
複製代碼

ArrayList的其餘方法

ensureCapacity方法

最好在 add 大量元素以前用 ensureCapacity 方法,以減小增量重新分配的次數

public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
複製代碼

ArrayList總結

(1)ArrayList 是一種變長的集合類,基於定長數組實現,使用默認構造方法初始化出來的容量是10(1.7以後都是延遲初始化,即第一次調用add方法添加元素的時候纔將elementData容量初始化爲10)。

(2)ArrayList 容許空值和重複元素,當往 ArrayList 中添加的元素數量大於其底層數組容量時,其會經過擴容機制從新生成一個更大的數組。ArrayList擴容的長度是原長度的1.5倍

(3)因爲 ArrayList 底層基於數組實現,因此其能夠保證在 O(1) 複雜度下完成隨機查找操做。

(4)ArrayList 是非線程安全類,併發環境下,多個線程同時操做 ArrayList,會引起不可預知的異常或錯誤。

(5)順序添加很方便

(6)刪除和插入須要複製數組,性能差(可使用LinkindList)

(7)Integer.MAX_VALUE - 8 :主要是考慮到不一樣的JVM,有的JVM會在加入一些數據頭,當擴容後的容量大於MAX_ARRAY_SIZE,咱們會去比較最小須要容量和MAX_ARRAY_SIZE作比較,若是比它大, 只能取Integer.MAX_VALUE,不然是Integer.MAX_VALUE -8。 這個是從jdk1.7開始纔有的

fast-fail機制

fail-fast的解釋:

在系統設計中,快速失效系統一種能夠當即報告任何可能代表故障的狀況的系統。快速失效系統一般設計用於中止正常操做,而不是試圖繼續可能存在缺陷的過程。這種設計一般會在操做中的多個點檢查系統的狀態,所以能夠及早檢測到任何故障。快速失敗模塊的職責是檢測錯誤,而後讓系統的下一個最高級別處理錯誤。

​ 就是在作系統設計的時候先考慮異常狀況,一旦發生異常,直接中止並上報,好比下面的這個簡單的例子

//這裏的代碼是一個對兩個整數作除法的方法,在fast_fail_method方法中,咱們對被除數作了個簡單的檢查,若是其值爲0,那麼就直接拋出一個異常,並明確提示異常緣由。這其實就是fail-fast理念的實際應用。
public int fast_fail_method(int arg1,int arg2){
    if(arg2 == 0){
        throw new RuntimeException("can't be zero");
    }
    return arg1/arg2;
}
複製代碼

​ 在Java集合類中不少地方都用到了該機制進行設計,一旦使用不當,觸發fail-fast機制設計的代碼,就會發生非預期狀況。咱們一般說的Java中的fail-fast機制,默認指的是Java集合的一種錯誤檢測機制。當多個線程對部分集合進行結構上的改變的操做時,有可能會觸發該機制時,以後就會拋出併發修改異常**ConcurrentModificationException**.固然若是不在多線程環境下,若是在foreach遍歷的時候使用add/remove方法,也可能會拋出該異常。參考fast-fail機制,這裏簡單作個總結

之因此會拋出ConcurrentModificationException異常,是由於咱們的代碼中使用了加強for循環,而在加強for循環中,集合遍歷是經過iterator進行的,可是元素的add/remove倒是直接使用的集合類本身的方法。這就致使iterator在遍歷的時候,會發現有一個元素在本身不知不覺的狀況下就被刪除/添加了,就會拋出一個異常,用來提示可能發生了併發修改!因此,在使用Java的集合類的時候,若是發生ConcurrentModificationException,優先考慮fail-fast有關的狀況,實際上這可能並無真的發生併發,只是Iterator使用了fail-fast的保護機制,只要他發現有某一次修改是未通過本身進行的,那麼就會拋出異常。

相關文章
相關標籤/搜索