Java基礎 ArrayList源碼分析 JDK1.8

1、概述

  本篇文章記錄經過閱讀JDK1.8 ArrayList源碼,結合自身理解分析其實現原理。java

  ArrayList容器類的使用頻率十分頻繁,它具備如下特性:數組

  • 其本質是一個數組,所以它是有序集合
  • 經過 get(int i) 下標獲取數組的指定元素時,時間複雜度是O(1)
  • 經過 add(E e)插入元素時,可直接向當前數組最後一個位置插入(這個描述不是特別準確,其中涉及到擴容、後續將講解),其時間複雜度爲O(1)
  • 經過 add(int i, E e)向指定位置插入元素時,是在原數組的基礎上經過拷貝和偏移量實現,其時間複雜度爲O(n)
  • 經過 remove(int i) 刪除指定位置的元素時,一樣也是在原數組的基礎上經過拷貝和偏移量實現,其時間複雜度爲O(n)
  • 使用內部類Itr()迭代刪除ArrayList刪除元素時,需使用迭代器的remove()方法,不能使用ArrayList.remove(Object o)方法
  • ArrayList是非線程安全的,可能形成數據丟失,數組越界的問題

2、源碼分析

1.重要屬性

private static final int DEFAULT_CAPACITY = 10;  //  擴容因子默認爲10

transient Object[] elementData;            // 元素數據

private int size;                    //  當前ArrayList中實際元素數量

protected transient int modCount = 0;        //  ArrayList被修改的次數,這個屬性是從java.util.AbstractList繼承下來的

   

2.重要操做

  構造器

/*  
 * 無參構造器 將elementData初始化爲一個空的數組
*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/*
 * int整形構造器,將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);
    }
 }

 

  boolean add(E e)  添加一個元素

 將添加數組分解成如下5個步驟
安全

//  添加一個元素時 必須確保當前數組能夠添加一個新的元素,所以會根據capacity去計算
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    
    // ⑤ 將新元素添加到 擴容以後的數組size++座標上,(注意是size++,先賦值再自增size)
    elementData[size++] = e;
    return true;
}


private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}


// ① 計算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    /* 若是初始化ArrayList時 是使用無參構造 new ArrayList()
那麼第一次向ArrayList添加元素時會將容量設爲DEFAULT_CAPACITY 10個*/
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // ② 確認是否擴容 private void ensureExplicitCapacity(int minCapacity) { // ArrayList被修改次數 + 1 modCount++; /*
若是計算出來的容量 > elementData數組的長度時,那麼會要擴容 由於elementData數組放不下新元素了;
不然的話就不須要擴容
*/ if (minCapacity - elementData.length > 0) grow(minCapacity); } // ③ 擴容 private void grow(int minCapacity) { // 新數組容量 = 老數組容量 * 1.5 int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); /* 可是必需要要保證 新容量 > ①中【計算出來的容量】 ,不然依然放不下新元素 若是出現這種狀況,那麼使用①中【計算出的容量】最爲新數組容量,保證能放下元素 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 數組元素過多 使用Integer.MAX_VALUE 做爲容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // ④ 經過拷貝的方式擴容 調用native層方法 elementData = Arrays.copyOf(elementData, newCapacity); }

 

  舉個例子

 

// 往ArrayList中插入11個元素
List<Object> list = new ArrayList<>();
for (int i = 0; i < 11; i++) {
    list.add(new Object());
}

 上述代碼很簡單,步驟爲:ide

  1. 調用無參構造器,初始化一個ArrayList
  2. 循環向該ArrayList插入11個元素

以該步驟爲例:源碼分析

  List<Object> list = new ArrayList<>();  //   初始化elementData爲一個空數組{} , elementData.length = 0ui

  i = 0 時this

  list.add(new Object());spa

  這個操做會經歷上面代碼片斷的①②③④⑤步驟線程

  1. 在走步驟①時使用默認容量 10
  2. 走步驟②時  判斷10 > elementData.length  所以使用10做爲最小容量請求擴容
  3. 走步驟③時 elementData.length 擴大1.5倍後依然爲0 所以使用10做爲新容量
  4. 經過步驟④native層拷貝方法進行數組拷貝
  5. 經過步驟⑤在新數組的size++ (即第個0個座標上),而且size自增爲1

 

  當 1 ≤ i ≤ 9時 3d

   list.add(new Object());

  在步驟①算計容量時,minCapacity依次計算出來爲 1, 2, 3, 4, 5, 6, 7, 8, 9 都小於elementData.length(當前經歷了第一次擴容 爲10) ,所以不會考慮擴容,直接將新增元素放到指定的下標中

  ps: 此步驟modCount每次都會+1

  當 i = 9時, 數組被填充滿

 

 

  當 i = 10 時

  list.add(new Object());

  此時就不能將第11個元素放到數組中了,須要第二次擴容

  1. 執行代碼片斷步驟①,算出minCapacity=11
  2. 步驟②判斷出minCapacity(11) > elementData.length(10),所以須要擴容
  3. 步驟③,經過在elementData.length基礎上擴大1.5倍 即 15, 同時大於 minCapacity, 所以使用15做爲新容量
  4. 步驟④,以15做爲新數組容量,拷貝原數組到新數組中
  5. 步驟⑤,將新的元素放到擴容以後新數組下標10的位置中,而且size自增

 

  

能夠看到,上述操做過程當中list經歷了兩次擴容,所以在使用ArrayList的時候能夠考慮使用有參構造器,確認ArrayList的大小,防止擴容發生,影響效率

 

    add(int i, E e)  向指定位置添加一個元素

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++;
}


private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

能夠看到向指定位置插入元素和add(E e)方法步驟差很少,只是拷貝方式作了改變

  1. 不發生擴容時插入過程

  2. 發生擴容時插入過程

    

    E remove(int index)  根據下標刪除元素

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);
    //    刪除最後一個元素
    elementData[--size] = null; // clear to let GC do its work

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

 

刪除元素時,是沒有縮容操做的,例如一個ArrayList中elementData僅有10個元素而且存滿了,remove掉9個元素後,size大小是1,可是elementData.length依舊爲10

   

    void trimToSize()  修整大小

//    這個方法能夠以當前elementData數組的size實際大小 從新拷貝出一個數組
//    防止上面問題的發生
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

 

 

    E get(int index)  從指定索引獲取元素

//    獲取指定元素
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

    這個方法很簡單,檢查完索引範圍,直接從elementData數組上去取對應位置的元素

 

    boolean contains(Object o)  查詢ArrayList中是否包含某個元素

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}


//    遍歷elementData數組 查詢知足條件的下標
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

能夠看到查詢ArrayList中是否包含某個元素時,是經過遍歷整個數組,依次查詢,所以該方法時間複雜度爲O(n)

    

    Iterator<E> iterator()  迭代

public Iterator<E> iterator() {
    return new Itr();
}

//    內部類 迭代器
private class Itr implements Iterator<E> {
    int cursor;       // 返回數據的遊標
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;    //    迭代器指望的被修改的次數 與ArrayList.modCount相同,

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //    ①  expectedModCount != modCount 時拋出ConcurrentModificationException
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    //    經過迭代器進行刪除,每刪除一個元素時expectedModCount會被從新賦值
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

前文中屢次提到ArrayList的屬性modCount,內部類Itr中提供的remove方法,在刪除元素成功後,會將expectedModCount進行更新,所以,經過迭代器迭代ArrayList時,不能使用ArrayList.remove(Object o)方法,若是直接使用ArrayList.remove(Object o)進行刪除(這個方法會對modCount減1),會致使迭代器進行下一次迭代時調用next()方法 檢查到expectedModCount與modCount不一樣(上述代碼①處),拋出ConcurrentModificationException,下面的代碼示例將展現正確與錯誤在迭代中刪除元素的操做

//    正確操做
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();    //    next()操做會更新遊標cursor
    iterator.remove();
}


//    錯誤操做:將拋出ConcurrentModificationException
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer next = iterator.next();
    list.remove(next);
}

 

3.線程安全問題(非線程安全)

/*  size++非原子操做  elementData[size++] = e  等同於
  elementData[size] = e;
  size = size + 1  */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

 

  ArrayList是非線程安全的,緣由是在add操做時,因爲size++是非原子操做,多個線程去對同一ArrayList執行add(E e)方法時,會出現下面兩種狀況

 

  1.狀況1:當兩個線程同時進入add()方法,讀取到當前size爲0,並都往0的下標存元素時,就會致使其中的一個值被覆蓋掉的問題


  2.狀況2:假設當前ArrayList爲elementData[10],可存放10個元素,而且當前已經存了9個元素了,那麼此時size=9

狀況1
線程A 線程B
add(e1) add(e2)
讀到size爲0 讀到size也爲0
elementData[0] = e1  
  elementData[0] = e2

 

 

 

狀況2
線程A 線程B
add(e1) add(e2)
讀到size爲9 不須要擴容 讀到size爲9 不須要擴容
elementData[9] = e1  

size=size+1//size = 10

 
 

elementData[10] = e2

// 因爲沒有擴容致使數組越界

... 拋出異常了
相關文章
相關標籤/搜索