淺談 ArrayList 及其擴容機制
淺談ArrayList
ArrayList類又稱動態陣列,同時實現了Collection和List介面,其內部資料結構由陣列實現,因此可對容器內元素實現快速隨機訪問。但因為ArrayList中插入或刪除一個元素需要移動其他元素,所以不適合在插入和刪除操作頻繁的場景下使用。
ArrayList的容量可以隨著元素的增加而自動增加,因此不用擔心ArrayList容量不足的問題。
ArrayList是非執行緒安全的。
接下來,我們將解析ArrayList的構造方法,在看構造方法之前,我們先來明確一下ArrayList原始碼中的一些概念。這些變數和物件大家先記住就好了,後面會看到它們的用途。
// 預設的容量大小(常量)
private static final int DEFAULT_CAPACITY = 10;
// 定義的空陣列(final修飾,大小固定為0)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 定義的預設空容量的陣列(final修飾,大小固定為0)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 定義的不可被序列化的陣列,實際儲存元素的陣列
transient Object[] elementData;
// 陣列中元素的個數
private int size;
ArrayList有三種構造方法:
1.無參的構造方法
2.根據傳入的數值大小,建立指定長度的陣列
3.通過傳入Collection元素列表進行生成
1.無參的構造方法
// 無參的構造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
可以看出來,當我們直接建立ArrayList時,elementData被賦予了預設空容量的陣列。注意,因為預設空容量陣列是被final修飾的,此時ArrayList陣列是空的、固定長度的
2.根據傳入的數值大小,建立指定長度的陣列
// 傳容量的構造方法
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);
}
}
當initialCapacity > 0時,會在堆上new一個大小為initialCapacity的陣列,然後將其引用賦給elementData,此時ArrayList的容量為initialCapacity,元素個數size為預設值0。
當initialCapacity = 0時,elementData被賦予了預設空陣列,因為其被final修飾了,所以此時ArrayList的容量為0,元素個數size為預設值0。
當initialCapacity < 0時,會丟擲異常。
3.通過傳入Collection元素列表進行生成
// 傳入Collection元素列表的構造方法
public ArrayList(Collection<? extends E> c) {
// 將列表轉化為對應的陣列
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 此處見下面詳細解析
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 賦予空陣列
this.elementData = EMPTY_ELEMENTDATA;
}
}
傳入Collection元素列表後,構造方法首先會將其轉化為陣列,將其索引賦給elementData。如果此陣列的長度為0,會重新賦予elementData為空陣列,此時ArrayList的容量是0,元素個數size為0。
如果陣列的長度不為0,會跟新size的大小為其長度,也就是元素的個數,然後執行裡面的程式。大家對裡面的程式碼可能不理解,讓我們等會看下面解析。執行完後此時ArrayList的容量為傳入序列的長度,也就是size的大小,同時元素個數也為size,也就是說,此時ArrayList是滿的。
讓我們來看看下面的程式碼,然後再去理解上面 if 語句的程式碼:
public class Test {
public static void main(String[] args) {
// 1.建立Student物件陣列
Student[] students = new Student[] {
new Student("小明", 18),
new Student("小李", 19),
new Student("小張", 21)
};
// 2.將其賦值給Object物件陣列
Object[] objects = students;
// 3.執行if語句前,列印陣列的class
System.out.println("執行前:" + objects.getClass());
// 4.執行上面的程式碼
if (objects.getClass() != Object[].class) {
objects = Arrays.copyOf(objects, objects.length, Object[].class);
}
// 5.執行if語句後,列印陣列的class
System.out.println("執行後:" + objects.getClass());
}
}
程式的執行結果如下:
可以看到,物件陣列也是有.class的,其中含有所儲存元素的型別,而上面的那段程式碼的作用就是將原物件陣列的陣列型別轉化為Object物件陣列的陣列型別,以便更好的儲存。
ArrayList的擴容機制
當我們探討擴容時,肯定要從ArrayList的add方法走起,讓我們來看看吧。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
這是最基本的add方法,當然,也是可以來說明問題的。可以看到,此add方法的引數就是一個被加元素,moCount是記錄ArrayList被修改的次數的,可以不用管。然後是另一個add方法,所傳的值是被加元素、當前陣列和當前陣列的元素個數,讓我們來看看這個add方法吧。
private void add(E e, Object[] elementData, int s) {
// 判斷元素個數是否等於當前容量
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
首先,它判斷了元素個數是否等於當前的容量,也就是判斷當前陣列空間是不是滿的,如果二者相等,則當前空間是滿的,就需要擴容了,grow函式就是擴容函式了,擴容後再將被加元素加到陣列中。
下面我們來看看grow函式是什麼樣子的:
private Object[] grow() {
return grow(size + 1);
}
它裡面呼叫了一個帶參的grow函式,引數是當前元素個數+1,也就是當前容量+1。返回的是這個函式的返回值,讓我們進一步研究。
private Object[] grow(int minCapacity) {
// 獲取老容量,也就是當前容量
int oldCapacity = elementData.length;
// 如果當前容量大於0 或者 陣列不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
// 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,建立新陣列
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
首先,它是記錄了一下老容量的大小,然後再進行下面的操作。
如果當前容量大於0,或者當前陣列不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面說明過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空的長度固定為0的陣列,在三個構造方法中,只有無參構造方法中elementData被賦予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是說,這個if語句中不會處理用預設無參構造方法建立的陣列的初始擴容情況,那麼其餘的情況都是由此if語句來處理的。
我們來看一下if裡面的操作,先建立一個新的陣列,然後將舊陣列拷貝到新陣列並賦給elementData返回。ArraysSupport.newLength函式的作用是建立一個大小為oldCapacity+max(minimum growth, preferred growth)的陣列。minCapacity是傳入的引數,我們上面看過,它的值是當前容量(老容量)+1,那麼minCapacity - oldCapacity的值恆為1,minimum growth的值恆為1;oldCapacity >> 1的功能是將oldCapacity 進行位操作,右移一位,也就是減半,preferred growth的值為oldCapacity 的一半。
當oldCapacity 為0時,右移後還是0,也就是說此時擴容的大小為0+max(1,0)=1,容量從0擴充套件到1,那麼什麼時候是這種情況呢?當傳容量的構造方法傳入的是0時,elementData被賦予的是EMPTY_ELEMENTDATA,陣列容量為0,此時新增元素時,符合if的條件,會進入此擴容情況,容量從0擴充套件到1。當傳Collection元素列表的構造方法被傳入空列表時,elementData被賦予的是EMPTY_ELEMENTDATA,陣列容量為0,此時新增元素時,符合if的條件,會進入此擴容情況,容量從0擴充套件到1。
當oldCapacity 大於0時,新建立的陣列大小是老容量+老容量的一半,也就是老容量的1.5倍,每次擴容到原來的1.5倍。
if之外就剩一種情況了,也就是用預設無參構造方法建立的陣列的初始擴容情況。此時的容量為0,新增一個元素時會建立一個新的陣列,其大小為max(DEFAULT_CAPACITY, minCapacity),我們從上面的原始碼變數資訊中可得知DEFAULT_CAPACITY的值為10,而minCapacity的值為1,所以新增一個元素時,max(DEFAULT_CAPACITY, minCapacity)的值必為10。也就是說,當我們用預設無參構造方法建立的陣列在新增元素前,ArrayList的容量為0,新增一個元素後,ArrayList的容量為10。
總結一下
ArrayList的特點:
1.ArrayList的底層資料結構是陣列,所以查詢遍歷快,增刪慢。
2.ArrayList可隨著元素的增長而自動擴容,正常擴容的話,每次擴容到原來的1.5倍。
3.ArrayList的執行緒是不安全的。
ArrayList的擴容:
在兩種情況下需要擴容:
第一種情況,當ArrayList的容量為0時,此時新增元素的話,需要擴容。什麼時候容量可能為0呢?當然是剛被建立的時候,三種構造方法建立的ArrayList在擴容時略有不同:
(1)無參構造,建立ArrayList後容量為0,新增第一個元素後,容量變為10,此後若需要擴容,則正常擴容。
(2)傳容量構造,當引數為0時,建立ArrayList後容量為0,新增第一個元素後,容量為1,此時ArrayList是滿的,下次新增元素時需正常擴容。
(3)傳列表構造,當列表為空時,建立ArrayList後容量為0,新增第一個元素後,容量為1,此時ArrayList是滿的,下次新增元素時需正常擴容。
第二種情況,當ArrayList的容量大於0時,此時新增元素若需要擴容的話,正常擴容,每次擴容到原來的1.5倍。