深刻了解Java集合中的ArrayList

什麼是ArrayList

ArrayList 是一個可擴容數組Resizable-array,它實現了List接口的全部方法。java

從對ArrayList的簡單描述中咱們能夠得出幾點數組

  1. ArrayList 是數組,但不一樣於通常數組,可擴容,而通常數組容量固定。
  2. ArrayList 實現了List接口,因此List包含的基本方法(新增,刪除,插入等)ArrayList都實現了。

ArrayList 須要知道的10點內容

1.ArrayList 內部是經過數組實現

ArrayList 內部實現是經過一個對象數組
transient Object[] elementData

2.ArrayList 添加操做(add) 的執行時間是固定的

ArrayList add() 方法實質上是實現了在數組的末尾添加一個元素 elementData[size++] = el 因此執行的時間複雜度是固定的 O(1),添加n個元素爲 O(n)

3.除了 add 方法外其餘操做如: 插入、刪除 操做時間是線性的。

由於本質上是對數組進行操做,當在數組中插入、刪除數據時,須要先對數組進行移位操做,插入時須要將插入點之後的數據所有後移,而刪除操做則須要將刪除節點後的數據所有前移,操做時間複雜度爲 O(n)

4.線程安全問題

ArrayList 在多線程狀況下有線程進行修改時,是線程不安全的。線程安全性問題,取決於如何應用。 List list = Collections.synchronizedList(new ArrayList(...))能夠獲取線程安全的ArrayList

5.關於擴容

ArrayList 有其自動擴容機制也能夠在預知要處理數據大小時手動擴容安全

1.自動擴容多線程

ArrayList 在新增元素時(add ,addAll操做)會先檢測當前內部數組elementData[]的容量是否足夠添加新元素,若是不足則擴容,ArrayList最大容量爲Integer.MAX_VALUE,自動擴容調用的流程以下併發

public boolean add(E e) {
    // 自動擴容,並記錄元素修改數量 ,元素修改數量主要是用於併發修改錯誤
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

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

// 返回一個數組大小的值 minCapacity 和默認的 DEFAULT_CAPACITY 中較大的那個
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 若是是空數組 經過new ArrayList()建立
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      // 默認大小是DEFAULT_CAPACITY =  10
      return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

實際擴充容量的片斷dom

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 將上面方法返回的大小和當前 elementData 大小進行比較,判斷是否須要擴容
    if (minCapacity - elementData.length > 0)
      grow(minCapacity);
}
private void grow(int minCapacity) { // 具體擴容代碼
    int oldCapacity = elementData.length;
    // 擴容後容量老是擴容前的大約1.5倍左右增量0.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
    // 若是擴容後超出了最大個數限制 則由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);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
          throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

2.手動擴容或者指定初始容量性能

ArrayList 手動擴容主要是調用ensureCapacity(int minCapacity)方法,除此以外還能在建立ArrayList時指定初始容量ui

建立時直接指定容量new ArrayList<>(initialCapacity)及其實現this

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

經過ensureCapacity(int minCapacity)方法在處理過程當中擴容,這種狀況通常發生在處理過程當中發現初始容量不能知足當前數據需求,而又爲了不自動擴容時的資源浪費,由於每次自動擴容時都會進行數組複製。線程

public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

3.最後看看擴容時的數組複製,數組複製是經過Arrays.copyOf(origin,newLenght)方法來實現的

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
// 具體複製代碼
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 將舊數組中的數據複製到新數組
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

6.關於ArrayList在方法中傳遞的問題。

ArrayList在方法中傳遞時,傳遞的是對象的引用,當某一個方法修改了ArrayList時,該修改會反映到引用該ArrayList的全部地方,無論傳遞多少次,都引用的時同一個內存區域的數據,因此若是要實如今傳遞時確保內容不變,應該克隆 (用ArrayList的clone())方法一份進行傳遞(須要注意深淺克隆問題深克隆可使用 Collections.copy(dest,src)方法),具體該怎麼弄,仍是要視需求而定。

7.ArrayList 的capacity和size的關係

若是要找他們之間的關係可能就是 size=元素數量 <= capacity = 容量大小吧。 size 反映的是當前數組中存了多少數據,而 capacity則反映的是ArrayList內部數組的容量。不能習慣性認爲 capacity 既是 size

8.ArrayList 和 LinkedList的選擇性問題

ArrayList和LinkedList爲何會存在選擇性問題,由於他們在 get、add、remove時性能是不同的。

ArrayList是內部實現是數組,在隨機存取時時間複雜度是O(1)知道索引就能立馬取值,而其插入和刪除操做就相對比較麻煩,須要進行移位操做線性的時間複雜度。

LinkedList 是雙向連表Doubly-linked ,在插入和刪除時時間複雜度都是O(1),但索引(取索引位上的值)時須要從表頭或者表尾進行遍歷操做。

因此選用哪個,徹底取決於你要進行的操做是以隨機存取爲主仍是增刪元素較多爲主。

9.爲何elementData使用transient修飾

transient關鍵字的做用是阻止對象序列化,那麼爲何要防止elementData序列化呢?那是由於elementData是一個數組,而且並非數組中每個位置上都保存有值,容量10000的數組中可能只保存了10個對象。因此不能對其進行序列化,在ArrayList中重寫了 writeObject 、readObject 方法來對ArrayList進行序列化控制。

10.ArrayList實現RandomAccess接口有什麼用

RandomAccess接口是一個 標記接口並無定義任何方法,ArrayList 實現它是標記ArrayList支持 快速隨機訪問這一特性!

11.併發修改異常ConcurrentModificationException

併發修改異常ConcurrentModificationException一般出如今對一個List進行遍歷時,對正在遍歷的數組進行了修改性操做(修改性操做:改變大小(size=數量)的操做,而不是指具體值)(add,remove,clear等)時便會拋出這個異常,在ArrayList內部有一個protected transient int modCount = 0的變量用於記錄對ArrayList的修改,好比add方法代碼

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

能夠看到在調用add方法時會執行modCount++操做以此標記最新修改的數量modCount

而在遍歷時好比forEach先看代碼

public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

能夠看到for循環中一個明確的條件是modCount == expectedModCount 每次遍歷都會檢測該條件是否成立,而在進入該段代碼以前先用final int expectedModCount = modCount;來保存代碼執行以前的修改數量,當進入遍歷後,有了更改操做,就會使得expectedModCount 和modCount不相等此時便會拋出ConcurrentModificationException異常,對於其餘的修改操做,原理都是相似的。

相關文章
相關標籤/搜索