從今天開始也正式開 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
獲取元素。 -
缺少一些函數式編程的方法。