「必知必會」最細緻的 ArrayList 原理分析
從今天開始也正式開 JDK 原理分析的坑了,其實寫原始碼分析的目的不再是像以前一樣搞懂原理,更重要的是看看他們編碼風格更進一步體會到他們的設計思想。看原始碼前先自己實現一個再比對也許會有不一樣的收穫!
1. 結構
首先我們需要對 ArrayList 有一個大致的瞭解就從結構來看看吧.
1. 繼承
該類繼承自 AbstractList 這個比較好說
2. 實現
這個類實現的介面比較多,具體如下:
- 首先這個類是一個 List 自然有 List 介面
- 然後由於這個類需要進行隨機訪問,所謂隨機訪問就是用下標任一訪問,所以實現了RandomAccess
- 然後就是兩個集合框架肯定會實現的兩個介面 Cloneable, Serializable 前面這個好說序列化一會我們具體再說說
3. 主要欄位
// 預設大小為10 private static final int DEFAULT_CAPACITY = 10; // 空陣列 private static final Object[] EMPTY_ELEMENTDATA = {}; // 預設的空陣列 這個是在傳入無參的是建構函式會呼叫的待會再 add 方法中會看到 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 用來存放 ArrayList 中的元素 注意他的修飾符是一個 transient 也就是不會自動序列化 transient Object[] elementData; // 大小 private int size;
4. 主要方法
下面的方法後面標有數字的就是表示過載方法
- ctor-3
- get
- set
- add-2
- remove-2
- clear
- addAll
- write/readObject
- fast-fail 機制
- subList
- iterator
- forEach
- sort
- removeIf
2. 構造方法分析
1. 無參的構造方法
裡面只有一個操作就是把 elementData
設定為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
這個空陣列。
// 無參的建構函式,傳入一個空陣列 這時候會建立一個大小為10的陣列,具體操作在 add 中 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
2. 傳入陣列大小的構造
這個就是 new 一個數組,如果陣列大小為0就 賦值為 EMPTY_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);
}
}
3. 傳入 Collection 介面
在這個方法裡面主要就是把這個 Collection 轉成一個數組,然後把這個陣列 copy 一下,如果這個介面的 size 為0 和上面那個方法一樣傳入 EMPTY_ELEMENTDATA
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 上面的註釋的意思是說 jdk 有一個 bug 具體來說就是一個 Object 型別的陣列不一定能夠存放 Object型別的物件,有可能拋異常
// 主要是因為 Object 型別的陣列可能指向的是他的子類的陣列,存 Object 型別的東西會報錯
if (elementData.getClass() != Object[].class)
// 這個操作是首先new 了新的陣列,然後再呼叫 System.arraycopy 拷貝值。也就是產生新的陣列
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 傳入的是空的就直接使用空陣列初始化
this.elementData = EMPTY_ELEMENTDATA;
}
}
但是注意一點這裡有一個 jdk 的 bug 也就是一個 Object 型別的陣列不一定能夠存放 Object型別的物件,有可能拋異常,主要是因為 Object 型別的陣列可能指向的是他的子類的陣列,存 Object 型別的東西會報錯。 為了測試這個 bug 寫了幾行程式碼測試一下。這個測試是通不過的,就是存在上面的原因。
一個典型的例子就是 我們建立一個 string 型別的 list 然後呼叫 toArray 方法發現返回的是一個 string[] 這時候自然就不能隨便存放元素了。
class A{
}
class B extends A {
}
public class JDKBug {
@Test
public void test1() {
B[] arrB = new B[10];
A[] arrA = arrB;
arrA[0]=new A();
}
}
3. 修改方法分析
1. Set 方法
這個方法也很簡單 ,首先進行範圍判斷,然後就是直接更新下標即可。
// 也沒啥好說的就是,設定新值返回老值
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
2. Add(E e) 方法
這個方法首先呼叫了 ensureCapacityInternal()
這個方法裡面就判斷了當前的 elementData
是否等於 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
如果是的話,就把陣列的大小設定為 10 然後進行擴容操作,這裡剛好解釋了為什麼採用無參構造的List 的大小是 10 ,這裡擴容操作呼叫的方法是 ensureExplicitCapacity
裡面就幹了一件事如果使用者指定的大小 大於當前長度就擴容,擴容的方法採用了 Arrays.copy
方法,這個方法實現原理是 new 出一個新的陣列,然後呼叫 System.arraycopy
拷貝陣列,最後返回新的陣列。
public boolean add(E e) {
// 當呼叫了無參構造,設定大小為10
ensureCapacityInternal(size + 1); // Increments modCount
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果當前陣列是預設空陣列就設定為 10和 size+1中的最小值
// 這也就是說為什麼說無參構造 new 的陣列大小是 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 若使用者指定的最小容量 > 最小擴充容量,則以使用者指定的為準,否則還是 10
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 1.5倍增長
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
3. Add(int index, E e) 方法
這個方法比較簡單和上面基本一樣,然後只是最後放元素的時候的操作不一樣,他是採用了 System.arraycopy 從自己向自己拷貝,目的就在於覆蓋元素。 注意一個規律這裡面只要涉及下標的操作的很多不是自己手寫 for 迴圈而是採用類似的拷貝覆蓋的方法。算是一個小技巧。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount
// 覆蓋
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
4. remove(int index)
同理這裡面還是用了拷貝覆蓋的技巧。 但是有一點注意的就是不用的節點需要手動的觸發 gc ,這也是在 Efftive Java 中作者舉的一個例子。
public E remove(int index) {
rangeCheck(index);
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;
}
5. remove(E e)
這個方法操作很顯然會判斷 e 是不是 null 如果是 null 的話直接採用 ==
比較,否則的話就直接呼叫 equals
方法然後執行拷貝覆蓋。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 覆蓋
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 呼叫 equals 方法
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
6. clear()
這個方法就幹了一件事,把陣列中的引用全都設定為 null 以便 gc 。而不是僅僅把 size 設定為 0 。
// gc 所有節點
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
7. addAll(Collection e)
這個沒啥好說的就是,採用轉陣列然後 copy
// 一個套路 只要涉及到 Collection介面的方法都是把這個介面轉成一個數組然後對陣列操作
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
4. 訪問方法分析
1. get
直接訪問陣列下標。
// 沒啥好說的直接去找陣列下標
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
2. subList
這個方法的實現比較有意思,他不是直接擷取一個新的 List 返回,而是在這個類的內部還有一個 subList 的內部類,然後這個類就記錄了 subList 的開始結束下標,然後返回的是這個 subList 物件。你可能會想返回的 subList 他不是 List 不會有問題嗎,這裡這個 subList 是繼承的 AbstractList 所以還是正確的。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
// subList 返回的是一個位置標記例項,就是在原來的陣列上放了一些標誌,沒有修改或者拷貝新的空間
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
// other functions .....
}
5. 其他功能方法
1. write/readObject
前面在介紹資料域的時候我就有標註 elementData 是一個 transition 的變數也就是在自動序列化的時候會忽略這個欄位。
然後我們又在原始碼中找到到了 write/readObject
方法,這兩個方法是用來序列化 elementData
中的每一個元素,也就是手動的對這個欄位進行序列化和反序列化。這不是多此一舉嗎?
既然要將ArrayList的欄位序列化(即將elementData序列化),那為什麼又要用transient修飾elementData呢?
回想ArrayList的自動擴容機制,elementData陣列相當於容器,當容器不足時就會再擴充容量,但是容器的容量往往都是大於或者等於ArrayList所存元素的個數。
比如,現在實際有了8個元素,那麼elementData陣列的容量可能是8x1.5=12,如果直接序列化elementData陣列,那麼就會浪費4個元素的空間,特別是當元素個數非常多時,這種浪費是非常不合算的。
所以ArrayList的設計者將elementData設計為transient,然後在writeObject方法中手動將其序列化,並且只序列化了實際儲存的那些元素,而不是整個陣列。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
2. fast-fail
所謂的 fast-fail
就是在我們進行 iterator
遍歷的時候不允許呼叫 Collection
介面的方法進行對容器修改,否則就會拋異常。這個實現的機制是在 iterator
中維護了兩個變數,分別是 modCount
和 expectedModCount
由於 Collection
介面的方法在每次修改操作的時候都會對 modCount++
所以如果在 iterator
中檢測到他們不相等的時候就拋異常。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
3. forEach
這個是一個函數語言程式設計的方法,看看他的引數 forEach(Consumer<? super E> action)
很有意思裡面接受是一個函式式的介面,我們裡面回調了 Consumer
的 accept
所以我們只需要傳入一個函式介面就能對每一個元素處理。
@Override
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();
}
}
寫了一段測試程式碼,但是這個方法不常用,主要是 Collection 是可以自己生成 Stream 物件,然後呼叫上面的方法即可。這裡提一下。
public class ArrayListTest {
@Test
public void foreach() {
ArrayList<Integer> list = new ArrayList<>();
list.add(2);
list.add(1);
list.add(4);
list.add(6);
list.forEach(System.out::print); //列印每一次元素。
}
}
4. sort
底層呼叫了 Arrays.sort 方法沒什麼好說的。
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
5. removeIf
這個和 forEach 差不多,就是回撥寫好了。
6. Vector
以上基本是把 ArrayList
的重要的方法和屬性介紹完了,我們已經比較清楚他底層的實現和資料結構了。然後提到 ArrayList
自然也少不了一個比較古老的容器 Vector
這個容器真的和 ArrayList
太像了。因為你會發現他們連繼承和實現的介面都是一樣的。但是也會有一些不同的地方,下面分條介紹一下。
-
在
Vector
中基本所有的方法都是synchronized
的方法,所以說他是執行緒安全的ArrayList
-
構造方法不一樣,在屬性中沒有兩個比較特殊的常量,所以說他的構造方法直接初始化一個容量為 10 的陣列。然後他有四個構造方法。
-
遍歷的介面不一樣。他還是有
iterator
的但是他以前的遍歷的方法是Enumeration
介面,通過elements
獲取Enumeration
然後使用hasMoreElements
和nextElement
獲取元素。 -
缺少一些函數語言程式設計的方法。