深入學習JDK原始碼系列之、ArrayList
前言
JDK原始碼解析系列文章,都是基於JDK8分析的,雖然JDK15馬上要出來了,但是JDK8我還不會,我...
類圖
- 實現了
RandomAccess
介面,可以隨機訪問 - 實現了
Cloneable
介面,可以克隆 - 實現了
Serializable
介面,可以序列化、反序列化 - 實現了
List
介面,是List
的實現類之一 - 實現了
Collection
介面,是Java Collections Framework
成員之一 - 實現了
Iterable
介面,可以使用for-each
迭代
屬性
// 序列化版本UID private static final long serialVersionUID = 8683452581122892189L; /** * 預設的初始容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 用於空例項的共享空陣列例項 * new ArrayList(0); */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 用於提供預設大小的例項的共享空陣列例項 * new ArrayList(); */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 儲存ArrayList元素的陣列緩衝區 * ArrayList的容量,是陣列的長度 * * non-private to simplify nested class access */ transient Object[] elementData; /** * ArrayList中元素的數量 */ private int size;
小朋友,你四否有很多問號?
- 為什麼空例項預設陣列有的時候是
EMPTY_ELEMENTDATA
,而又有的時候是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- 為什麼
elementData
要用transient
修飾? - 為什麼
elementData
沒有被private
修飾?難道正如註釋所寫的non-private to simplify nested class access
帶著問題,我們繼續往下看。
構造方法
帶初始容量的構造方法
/** * 帶一個初始容量引數的構造方法 * * @param initialCapacity 初始容量 * @throws 如果初始容量非法就丟擲 * IllegalArgumentException */ 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
,就建立一個新的長度是initialCapacity
的陣列 - 如果
initialCapacity == 0
,就使用EMPTY_ELEMENTDATA - 其他情況,
initialCapacity
不合法,丟擲異常
無參構造方法
/** * 無參構造方法 將elementData 賦值為 * DEFAULTCAPACITY_EMPTY_ELEMENTDATA */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
帶一個集合引數的構造方法
/**
* 帶一個集合引數的構造方法
*
* @param c 集合,代表集合中的元素會被放到list中
* @throws 如果集合為空,丟擲NullPointerException
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
// 如果 size != 0
if ((size = elementData.length) != 0) {
// c.toArray 可能不正確的,不返回 Object[]
// https://bugs.openjdk.java.net/browse/JDK-6260652
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(
elementData, size, Object[].class);
} else {
// size == 0
// 將EMPTY_ELEMENTDATA 賦值給 elementData
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 使用將集合轉換為陣列的方法
- 為了防止
c.toArray()
方法不正確的執行,導致沒有返回Object[]
,特殊做了處理 - 如果陣列大小等於
0
,則使用EMPTY_ELEMENTDATA
那麼問題來了,什麼情況下
c.toArray()
會不返回Object[]
呢?
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("list"));
// class java.util.ArrayList
System.out.println(list.getClass());
Object[] listArray = list.toArray();
// class [Ljava.lang.Object;
System.out.println(listArray.getClass());
listArray[0] = new Object();
System.out.println();
List<String> asList = Arrays.asList("asList");
// class java.util.Arrays$ArrayList
System.out.println(asList.getClass());
Object[] asListArray = asList.toArray();
// class [Ljava.lang.String;
System.out.println(asListArray.getClass());
// java.lang.ArrayStoreException
asListArray[0] = new Object();
}
我們通過這個例子可以看出來,java.util.ArrayList.toArray()
方法會返回Object[]
沒有問題。而java.util.Arrays
的私有內部類ArrayList的toArray()
方法可能不返回Object[]
。
為什麼會這樣?
我們看ArrayList的toArray()
方法原始碼:
public Object[] toArray() {
// ArrayLisy中 elementData是這樣定義的
// transient Object[] elementData;
return Arrays.copyOf(elementData, size);
}
使用了Arrays.copyOf()
方法:
public static <T> T[] copyOf(T[] original, int newLength) {
// original.getClass() 是 class [Ljava.lang.Object
return (T[]) copyOf(original, newLength, original.getClass());
}
copyOf()
的具體實現:
public static <T,U> T[] copyOf(U[] original,
int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
/**
* 如果newType是Object[] copy 陣列 型別就是 Object
* 否則就是 newType 型別
*/
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
我們知道ArrayList中elementData
就是Object[]
型別,所以ArrayList的toArray()
方法必然會返回Object[]
。
我們再看一下java.util.Arrays
的內部ArrayList原始碼(擷取的部分原始碼):
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable {
// 儲存元素的陣列
private final E[] a;
ArrayList(E[] array) {
// 直接把接收的陣列 賦值 給 a
a = Objects.requireNonNull(array);
}
/**
* obj 為空丟擲異常
* 不為空 返回 obj
*/
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
@Override
public Object[] toArray() {
// 返回 a 的克隆物件
return a.clone();
}
}
這是Arrays.asList()
方法原始碼
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
不難看出來java.util.Arrays
的內部ArrayList的toArray()
方法,是構造方法接收什麼型別的陣列,就返回什麼型別的陣列。
所以,在我們上面的例子中,實際上返回的是String型別的陣列,再將其中的元素賦值成Object
型別的,自然報錯。
我們還是繼續看ArrayList吧...
插入方法
在列表最後新增指定元素
/**
* 在列表最後新增指定元素
*
* @param e 要新增的指定元素
* @return true
*/
public boolean add(E e) {
// 增加 modCount !!
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
- 在父類
AbstractList
上,定義了modCount
屬性,用於記錄陣列修改的次數。
在指定位置新增指定元素
/**
* 在指定位置新增指定元素
* 如果指定位置已經有元素,就將該元素和隨後的元素移動到右面一位
*
* @param index 待插入元素的下標
* @param element 待插入的元素
* @throws 可能丟擲 IndexOutOfBoundsException
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
// 增加 modCount !!
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
插入方法呼叫的其他私有方法
/**
* 計算容量
*/
private static int calculateCapacity(
Object[] elementData, int minCapacity) {
if (elementData ==
DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(
calculateCapacity(elementData, minCapacity)
);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
擴容方法
/**
* 陣列可以分配的最大size
* 一些虛擬機器在陣列中預留一些header words
* 如果嘗試分配更大的size,可能導致OutOfMemoryError
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 增加容量,至少保證比minCapacity大
* @param minCapacity 期望的最小容量
*/
private void grow(int minCapacity) {
// 有可能溢位的程式碼
int oldCapacity = elementData.length;
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);
}
/**
* 最大容量返回 Integer.MAX_VALUE
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 通常情況新容量是原來容量的1.5倍
- 如果原容量的1.5倍比
minCapacity
小,那麼就擴容到minCapacity
- 特殊情況擴容到
Integer.MAX_VALUE
看完構造方法、新增方法、擴容方法之後,上文第1個問題終於有了答案。原來,
new ArrayList()
會將elementData
賦值為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,new ArrayList(0)
會將elementData
賦值為 EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA新增元素會擴容到容量為1
,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA擴容之後容量為10
。
通過反射我們可以驗證這一想法。如下:
public static void main(String[] args) {
printDefaultCapacityList();
printEmptyCapacityList();
}
public static void printDefaultCapacityList() {
ArrayList defaultCapacity = new ArrayList();
System.out.println(
"default 初始化長度:" + getCapacity(defaultCapacity));
defaultCapacity.add(1);
System.out.println(
"default add 之後 長度:" + getCapacity(defaultCapacity));
}
public static void printEmptyCapacityList() {
ArrayList emptyCapacity = new ArrayList(0);
System.out.println(
"empty 初始化長度:" + getCapacity(emptyCapacity));
emptyCapacity.add(1);
System.out.println(
"empty add 之後 長度:" + getCapacity(emptyCapacity));
}
public static int getCapacity(ArrayList<?> arrayList) {
Class<ArrayList> arrayListClass = ArrayList.class;
try {
// 獲取 elementData 欄位
Field field = arrayListClass.getDeclaredField("elementData");
// 開啟訪問許可權
field.setAccessible(true);
// 把示例傳入get,獲取例項欄位elementData的值
Object[] objects = (Object[]) field.get(arrayList);
//返回當前ArrayList例項的容量值
return objects.length;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
移除方法
移除指定下標元素方法
/**
* 移除列表中指定下標位置的元素
* 將所有的後續元素,向左移動
*
* @param 要移除的指定下標
* @return 返回被移除的元素
* @throws 下標越界會丟擲IndexOutOfBoundsException
*/
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);
// 將引用置空,讓GC回收
elementData[--size] = null;
return oldValue;
}
移除指定元素方法
/**
* 移除第一個在列表中出現的指定元素
* 如果存在,移除返回true
* 否則,返回false
*
* @param o 指定元素
*/
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++)
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);
// 將引用置空,讓GC回收
elementData[--size] = null;
}
查詢方法
查詢指定元素的所在位置
/**
* 返回指定元素第一次出現的下標
* 如果不存在該元素,返回 -1
* 如果 o ==null 會特殊處理
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
查詢指定位置的元素
/**
* 返回指定位置的元素
*
* @param index 指定元素的位置
* @throws index越界會丟擲IndexOutOfBoundsException
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
該方法直接返回
elementData
陣列指定下標的元素,效率還是很高的。所以ArrayList,for
迴圈遍歷效率也是很高的。
序列化方法
/**
* 將ArrayLisy例項的狀態儲存到一個流裡面
*/
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);
// 按照順序寫入所有的元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
反序列化方法
/**
* 根據一個流(引數)重新生成一個ArrayList
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt();
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
看完序列化,反序列化方法,我們終於又能回答開篇的第二個問題了。
elementData
之所以用transient
修飾,是因為JDK不想將整個elementData
都序列化或者反序列化,而只是將size
和實際儲存的元素序列化或反序列化,從而節省空間和時間。
建立子陣列
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;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
// 省略程式碼...
}
- SubList的set()方法,是直接修改ArrayList中
elementData
陣列的,使用中應該注意 - SubList是沒有實現
Serializable
介面的,是不能序列化的
迭代器
建立迭代器方法
public Iterator<E> iterator() {
return new Itr();
}
Itr屬性
// 下一個要返回的元素的下標
int cursor;
// 最後一個要返回元素的下標 沒有元素返回 -1
int lastRet = -1;
// 期望的 modCount
int expectedModCount = modCount;
Itr的hasNext() 方法
public boolean hasNext() {
return cursor != size;
}
Itr的next()方法
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在迭代的時候,會校驗
modCount
是否等於expectedModCount
,不等於就會丟擲著名的ConcurrentModificationException
異常。什麼時候會丟擲ConcurrentModificationException
?
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
remove(arrayList);
System.out.println(arrayList);
}
public static void remove(ArrayList<Integer> list) {
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer number = iterator.next();
if (number % 2 == 0) {
// 丟擲ConcurrentModificationException異常
list.remove(number);
}
}
}
那怎麼寫才能不丟擲
ConcurrentModificationException
?很簡單,將list.remove(number);
換成iterator.remove();
即可。why?請看Itr的remove()
原始碼...
Itr的remove()方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 移除之後將modCount 重新賦值給 expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
原因就是因為Itr的remove()
方法,移除之後將modCount
重新賦值給 expectedModCount
。這就是原始碼,不管單執行緒還是多執行緒,只要違反了規則,就會拋異常。
原始碼看的差不多了,開篇的問題卻還剩一個!到底為什麼
elementData
沒有用private
修飾呢?
我們知道的,private
修飾的變數,內部類也是可以訪問到的。難道註釋中non-private to simplify nested class access
的這句話有毛病?
當我們看表面看不到什麼東西的時候,不妨看一下底層。
測試類程式碼:
一頓javac
、javap
之後(使用JDK8):
再一頓javac
、javap
之後(使用JDK11):
雖然位元組碼指令我還看不太懂,但是我能品出來,註釋是沒毛病的,private
修飾的確會影響內部類的訪問。
ArrayList類註釋翻譯
類註釋還是要看的,能給我們一個整體的瞭解這個類。我將ArrayList的類註釋大概翻譯整理了一下:
- ArrayList是實現
List
介面的可自動擴容的陣列。實現了所有的List
操作,允許所有的元素,包括null
值。 - ArrayList大致和Vector相同,除了ArrayList是非同步的。
size
isEmpty
get
set
iterator
和listIterator
方法時間複雜度是O(1)
,常量時間。其他方法是O(n)
,線性時間。- 每一個ArrayList例項都有一個
capacity
(容量)。capacity
是用於儲存列表中元素的陣列的大小。capacity
至少和列表的大小一樣大。 - 如果多個執行緒同時訪問ArrayList的例項,並且至少一個執行緒會修改,必須在外部保證ArrayList的同步。修改包括新增刪除擴容等操作,僅僅設定值不包括。這種場景可以用其他的一些封裝好的同步的
list
。如果不存在這樣的Object
,ArrayList應該用Collections.synchronizedList
包裝起來最好在建立的時候就包裝起來,來保證同步訪問。 iterator()
和listIterator(int)
方法是fail-fast
的,如果在迭代器建立之後,列表進行結構化修改,迭代器會丟擲ConcurrentModificationException
。- 面對併發修改,迭代器快速失敗、清理,而不是在未知的時間不確定的情況下冒險。請注意,快速失敗行為不能被保證。通常來講,不能同步進行的併發修改幾乎不可能做任何保證。因此,寫依賴這個異常的程式的程式碼是錯誤的,快速失敗行為應該僅僅用於防止
bug
。
總結
- ArrayList底層的資料結構是陣列
- ArrayList可以自動擴容,不傳初始容量或者初始容量是
0
,都會初始化一個空陣列,但是如果新增元素,會自動進行擴容,所以,建立ArrayList的時候,給初始容量是必要的 Arrays.asList()
方法返回的是的Arrays
內部的ArrayList,用的時候需要注意subList()
返回內部類,不能序列化,和ArrayList共用同一個陣列- 迭代刪除要用,迭代器的
remove
方法,或者可以用倒序的for
迴圈 - ArrayList重寫了序列化、反序列化方法,避免序列化、反序列化全部陣列,浪費時間和空間
elementData
不使用private
修飾,可以簡化內部類的訪問
原始碼系列第一篇,一不小心就寫的有點長。但是懵懂到深刻的過程還是挺耐人尋味的。文章中沒有展開的點,或者你有什麼其他好奇的地方,歡迎留言討論。我們下篇文章再見...
歡迎關注個人微信公眾號【如逆水行舟】,用心輸出基礎、演算法、原始碼系列文章。