ArrayList概述
(1)ArrayList
是一種變長的集合類,基於定長陣列實現。
(2)ArrayList
允許空值和重複元素,當往 ArrayList 中新增的元素數量大於其底層陣列容量時,其會通過擴容機制重新生成一個更大的陣列。
(3)ArrayList
底層基於陣列實現,所以其可以保證在 O(1)
複雜度下完成隨機查詢操作。
(4)ArrayList
是非執行緒安全類,併發環境下,多個執行緒同時操作 ArrayList,會引發不可預知的異常或錯誤。
ArrayList的成員屬性
在介紹關於ArrayList的各種方法之前先看一下基礎屬性成員。其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA與EMPTY_ELEMENTDA他的區別是:當我們向陣列中新增第一個元素時,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_ELEMENTDATA
和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList構造方法
(1)帶有初始化容量的構造方法
- 引數大於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)無參構造
- 構造方法中將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。 - 假設又新增3、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)方法(在元素序列指定位置(假設該位置合理
)插入)的過程大概是下面這些
- 檢測陣列是否有足夠的空間(這裡的實現和上面的)
- 將 index 及其之後的所有元素向後移一位
- 將新元素插入至 index 處.
將新元素插入至序列指定位置,需要先將該位置及其之後的元素都向後移動一位,為新元素騰出位置。這個操作的時間複雜度為O(N)
,頻繁移動元素可能會導致效率問題,特別是集合中元素數量較多時。在日常開發中,若非所需,我們應當儘量避免在大集合中呼叫第二個插入方法。
ArrayList的remove方法
ArrayList支援兩種刪除元素的方式
1、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);
刪除過程如下圖所示
2、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的解釋:
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.
大概意思是:在系統設計中,快速失效系統一種可以立即報告任何可能表明故障的情況的系統。快速失效系統通常設計用於停止正常操作,而不是試圖繼續可能存在缺陷的過程。這種設計通常會在操作中的多個點檢查系統的狀態,因此可以及早檢測到任何故障。快速失敗模組的職責是檢測錯誤,然後讓系統的下一個最高階別處理錯誤。
其實就是在做系統設計的時候先考慮異常情況,一旦發生異常,直接停止並上報,比如下面的這個簡單的例子
//這裡的程式碼是一個對兩個整數做除法的方法,在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的保護機制,只要他發現有某一次修改是未經過自己進行的,那麼就會丟擲異常。