騰訊一面!說說ArrayList的遍歷foreach與iterator時remove的區別,我一臉懵逼
阿新 • • 發佈:2020-09-29
本文基於JDK-8u261原始碼分析
# 1 簡介
ArrayList作為最基礎的集合類,其底層是使用一個動態陣列來實現的,這裡“動態”的意思是可以動態擴容(雖然ArrayList可以動態擴容,但卻不會動態縮容)。但是與HashMap不同的是,ArrayList使用的是*1.5的擴容策略,而HashMap使用的是*2的方式。還有一點與HashMap不同:ArrayList的預設初始容量為10,而HashMap為16。
有意思的一點是:在Java 7之前的版本中,ArrayList的無參構造器是在構造器階段完成的初始化;而從Java 7開始,改為了在add方法中完成初始化,也就是所謂的**延遲初始化**。在HashMap中也有同樣的設計思路。
另外,同HashMap一樣,如果要存入一個很大的資料量並且事先知道要存入的這個資料量的固定值時,就可以往構造器裡傳入這個初始容量,以此來避免以後的頻繁擴容。
------
# 2 構造器
```java
/**
* ArrayList:
* 無參構造器
*/
public ArrayList() {
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現“{}”,這裡也就是在做初始化
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 有參構造器
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//initialCapacity>0就按照這個容量來初始化陣列
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//EMPTY_ELEMENTDATA也是一個空實現“{}”,這裡也是在做初始化
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果initialCapacity為負數,則丟擲異常
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
```
# 3 add方法
## 3.1 add(E e)
新增指定的元素:
```java
/**
* ArrayList:
*/
public boolean add(E e) {
//檢視是否需要擴容
ensureCapacityInternal(size + 1);
//size記錄的是當前元素的個數,這裡就直接往陣列最後新增新的元素就行了,之後size再+1
elementData[size++] = e;
return true;
}
/**
* 第6行程式碼處:
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
/*
minCapacity = size + 1
之前說過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現“{}”,這裡也就是在判斷是不是呼叫的無參構造器
並第一次呼叫到此處
*/
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
/*
如果是的話就返回DEFAULT_CAPACITY(10)和size+1之間的較大者。也就是說,陣列的最小容量是10
這裡有意思的一點是:呼叫new ArrayList<>()和new ArrayList<>(0)兩個構造器會有不同的預設容量(在HashMap中
也是如此)。也就是說無參構造器的初始容量為10,而傳進容量為0的初始容量為1。同時這也就是為什麼會有
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA這兩個常量的存在,雖然它們的值都是“{}”
原因就在於無參構造器和有參構造器完全就是兩種不同的實現策略:如果你想要具體的初始容量,那麼就呼叫有參構造器吧,
即使傳入的是0也是符合這種情況的;而如果你不在乎初始的容量是多少,那麼就呼叫無參構造器就行了,這會給你默
認為10的初始容量
*/
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//如果呼叫的是有參構造器,或者呼叫無參構造器但不是第一次進來,就直接返回size+1
return minCapacity;
}
/**
* 第16行程式碼處:
*/
private void ensureExplicitCapacity(int minCapacity) {
//修改次數+1(快速失敗機制)
modCount++;
/*
如果+1後期望的容量比實際陣列的容量還大,就需要擴容了(如果minCapacity也就是size+1後發生了資料溢位,
那麼minCapacity就變為了一個負數,並且是一個接近int最小值的數。而此時的elementData.length也會是一個接近
int最大值的數,那麼該if條件也有可能滿足,此時會進入到grow方法中的hugeCapacity方法中丟擲溢位錯誤)
*/
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//獲取擴容前的舊陣列容量
int oldCapacity = elementData.length;
//這裡擴容後新陣列的容量是採用舊陣列容量*1.5的方式來實現的
int newCapacity = oldCapacity + (oldCapacity >> 1);
/*
如果新陣列容量比+1後期望的容量還要小,此時把新陣列容量修正為+1後期望的容量(對應於newCapacity為0或1的情況)
這裡以及後面的判斷使用的都是“if (a - b < 0)”形式,而不是常規的“if (a < b)”形式是有原因的,
原因就在於需要考慮資料溢位的情況:如果執行了*1.5的擴容策略後newCapacity發生了資料溢位,那麼它就一樣
變為了一個負數,並且是一個接近int最小值的數。而minCapacity此時也必定會是一個接近int最大值的數,
那麼此時的“newCapacity - minCapacity”計算出來的結果就可能會是一個大於0的數。於是這個if條件
就不會執行,而是會在下個條件中的hugeCapacity方法中處理這種溢位的問題。這同上面的分析是類似的
而如果這裡用的是“if (newCapacity < minCapacity)”,資料溢位的時候該if條件會返回true,於是
newCapacity會錯誤地賦值為minCapacity,而沒有使用*1.5的擴容策略
*/
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
/*
如果擴容後的新陣列容量比設定好的容量最大值(Integer.MAX_VALUE - 8)還要大,就重新設定一下新陣列容量的上限
同上面的分析,如果發生資料溢位的話,這裡的if條件也可能是滿足的,那麼也會走進hugeCapacity方法中去處理
*/
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
/*
可以看到這裡是通過Arrays.copyOf(System.arraycopy)的方式來進行陣列的拷貝,
容量是擴容後的新容量newCapacity,將拷貝後的新陣列賦值給elementData即可
*/
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 第83行程式碼處:
*/
private static int hugeCapacity(int minCapacity) {
//minCapacity對應於size+1,所以如果minCapacity<0就說明發生了資料溢位,就丟擲錯誤
if (minCapacity < 0)
throw new OutOfMemoryError();
/*
如果minCapacity大於MAX_ARRAY_SIZE,就返回int的最大值,否則返回MAX_ARRAY_SIZE
不管返回哪個,這都會將newCapacity重新修正為一個大於0的數,也就是處理了資料溢位的情況
其實從這裡可以看出:本方法中並沒有使用*1.5的擴容策略,而只是設定了一個上限而已。但是在Java中
真能申請得到Integer.MAX_VALUE這麼大的陣列空間嗎?其實不見得,這只是一個理論值。實際上需要考慮
-Xms和-Xmx等一系列JVM引數所設定的值。所以這也可能就是MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
其中-8的含義吧。但不管如何,當陣列容量達到這麼大的量級時,乘不乘1.5其實已經不太重要了)
*/
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
```
## 3.2 add(int index, E element)
在指定的位置處新增指定的元素:
```java
/**
* ArrayList:
*/
public void add(int index, E element) {
//index引數校驗
rangeCheckForAdd(index);
//檢視是否需要擴容
ensureCapacityInternal(size + 1);
/*
這裡陣列拷貝的意義,就是將index位置處以及後面的陣列元素往後移動一位,以此來挪出一個位置
System.arraycopy是直接對記憶體進行復制,在大資料量下,比for迴圈更快
*/
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//然後將需要插入的元素插入到上面挪出的index位置處就可以了
elementData[index] = element;
//最後size+1,代表添加了一個元素
size++;
}
/**
* 第6行程式碼處:
* 檢查傳入的index索引位是否越界,如果越界就拋異常
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + size;
}
```
# 4 get方法
```java
/**
* ArrayList:
*/
public E get(int index) {
//index引數校驗
rangeCheck(index);
//獲取資料
return elementData(index);
}
/**
* 第6行程式碼處:
* 這裡只檢查了index大於等於size的情況,而index為負數的情況
* 在elementData方法中會直接丟擲ArrayIndexOutOfBoundsException
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 第8行程式碼處:
* 可以看到,這裡是直接從elementData陣列中獲取指定index位置的資料
*/
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
```
# 5 remove方法
## 5.1 remove(Object o)
刪除指定的元素:
```java
/**
* ArrayList:
*/
public boolean remove(Object o) {
if (o == null) {
//如果要刪除的元素為null
for (int index = 0; index < size; index++)
//遍歷陣列中的每一個元素,找到第一個為null的元素
if (elementData[index] == null) {
/*
刪除這個元素,並返回true。這裡也就是在做清理的工作:遇到一個為null的元素就清除掉
注意這裡只會清除一次,並不會全部清除
*/
fastRemove(index);
return true;
}
} else {
//如果要刪除的元素不為null
for (int index = 0; index < size; index++)
//找到和要刪除的元素是一致的陣列元素
if (o.equals(elementData[index])) {
/*
找到了一個就進行刪除,並返回true。注意這裡只會找到並刪除一個元素,
如果要刪除所有的元素就呼叫removeAll方法即可
*/
fastRemove(index);
return true;
}
}
/*
如果要刪除的元素為null並且找不到為null的元素,或者要刪除的元素不為null並且找不到和要刪除元素相等的陣列元素,
就說明此時不需要刪除元素,直接返回false就行了
*/
return false;
}
/**
* 第14行和第26行程式碼處:
*/
private void fastRemove(int index) {
//修改次數+1
modCount++;
//numMoved記錄的是移動元素的個數
int numMoved = size - index - 1;
if (numMoved > 0)
/*
這裡陣列拷貝的意義,就是將index+1位置處以及後面的陣列元素往前移動一位,
這會將index位置處的元素被覆蓋,也就是做了刪除
*/
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
/*
因為上面是左移了一位,所以最後一個位置相當於騰空了,這裡也就是將最後一個位置(--size)置為null
當然如果上面計算出來的numMoved本身就小於等於0,也就是index大於等於size-1的時候(大於不太可能,
是屬於異常的情況),意味著不需要進行左移。此時也將最後一個位置置為null就行了。置為null之後,
原有資料的引用就會被斷開,GC就可以工作了
*/
elementData[--size] = null;
}
```
## 5.2 remove(int index)
刪除指定位置處的元素:
```java
/**
* ArrayList:
*/
public E remove(int index) {
//index引數校驗
rangeCheck(index);
//修改次數+1
modCount++;
//獲取指定index位置處的元素
E oldValue = elementData(index);
//numMoved記錄的是移動元素的個數
int numMoved = size - index - 1;
if (numMoved > 0)
/*
同上面fastRemove方法中的解釋,這裡同樣是將index+1位置處以及後面的陣列元素往前移動一位,
這會將index位置處的元素被覆蓋,也就是做了刪除(這裡是否可以考慮封裝?)
*/
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
//同上,將最後一個位置(--size)置為null
elementData[--size] = null;
//刪除之後,將舊值返回就行了
return oldValue;
}
```
# 6 不要在foreach迴圈裡進行元素的remove/add操作
這是《阿里巴巴編碼規範》中的一條。正例:
```j