?史上最全的Java容器集合之ArrayList(原始碼解讀)
前言
文字已收錄至我的GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習群,群聊號碼:549684836 鼓勵大家在技術的路上寫部落格
絮叨
前面2篇的基礎,大家還是好好學習一下,下面是連結
?史上最全的Java容器集合之入門
?史上最全的Java容器集合之基礎資料結構(手撕連結串列)
本來想直接將List這個父類下的所有之類,但是怕太長,我把我們真實開發中最最常用的ArrayList單獨拿出來講了,後面2個做一篇(順便提一下,如果是零基礎的不建議來,有過半年工作經驗的跟著我一起把這些過一遍的話,對你的幫助是非常大的)
一、ArrayList認識
概念
概念:ArrayList是一個其容量能夠動態增長的動態陣列。但是他又和陣列不一樣,下面會分析對比。它繼承了AbstractList,實現了List、RandomAccess,Cloneable,java.io.Serializable。
RandomAccess介面,被List實現之後,為List提供了隨機訪問功能,也就是通過下標獲取元素物件的功能。
實現了Cloneable,java.io.Serializable意味著可以被克隆和序列化。
最主要的還是看我化圈的部分
ArrayList的資料結構
分析一個類的時候,資料結構往往是它的靈魂所在,理解底層的資料結構其實就理解了該類的實現思路,具體的實現細節再具體分析。
ArrayList的資料結構是:
說明:底層的資料結構就是陣列,陣列元素型別為Object型別,即可以存放所有型別資料。我們對ArrayList類的例項的所有的操作底層都是基於陣列的。ArrayList原始碼分析
繼承結構和層次關係
我們看一下ArrayList的繼承結構:
- ArrayList extends AbstractList
- AbstractList extends AbstractCollection
複製程式碼
所有類都繼承Object 所以ArrayList的繼承結構就是上圖這樣
思考 為什麼要先繼承AbstractList,而讓AbstractList先實現List?而不是讓ArrayList直接實現List?
這裡是有一個思想,介面中全都是抽象的方法,而抽象類中可以有抽象方法,還可以有具體的實現方法,正是利用了這一點,讓AbstractList是實現介面中一些通用的方法,而具體的類, 如ArrayList就繼承這個AbstractList類,拿到一些通用的方法,然後自己在實現一些自己特有的方法,這樣一來,讓程式碼更簡潔,就繼承結構最底層的類中通用的方法都抽取出來,先一起實現了,減少重複程式碼。所以一般看到一個類上面還有一個抽象類,應該就是這個作用
RandomAccess這個介面
我點進去原始碼發現他是一個空的介面,為啥是個空介面呢
RandomAccess介面是一個標誌介面(Marker)
ArrayList集合實現這個介面,就能支援快速隨機訪問
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list,T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list,key);
else
return Collections.iteratorBinarySearch(list,key);
}
複製程式碼
通過檢視原始碼,發現實現RandomAccess介面的List集合採用一般的for迴圈遍歷,而未實現這介面則採用迭代器。 ArrayList用for迴圈遍歷比iterator迭代器遍歷快,LinkedList用iterator迭代器遍歷比for迴圈遍歷快, 所以說,當我們在做專案時,應該考慮到List集合的不同子類採用不同的遍歷方式,能夠提高效能! 然而有人發出疑問了,那怎麼判斷出接收的List子類是ArrayList還是LinkedList呢? 這時就需要用instanceof來判斷List集合子類是否實現RandomAccess介面!
總結:RandomAccess介面這個空架子的存在,是為了能夠更好地判斷集合是否ArrayList或者LinkedList,從而能夠更好選擇更優的遍歷方式,提高效能
ArrayList 類中的屬性
public class ArrayList<E> extends AbstractList<E>
implements List<E>,RandomAccess,java.io.Serializable
{
// 版本號
private static final long serialVersionUID = 8683452581122892189L;
// 預設容量
private static final int DEFAULT_CAPACITY = 10;
// 空物件陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
// 預設空物件陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素陣列 ArrayList中有擴容這麼一個概念,正因為它擴容,所以它能夠實現“動態”增長
transient Object[] elementData;
// 實際元素大小,預設為0
private int size;
// 最大陣列容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
複製程式碼
構造方法
無參構造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製程式碼
此時ArrayList的size為空,但是elementData的length為1。第一次新增時,容量變成初始容量大小10(預設無參構造的容量就是10)
int引數構造
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);
}
}
複製程式碼
這個比較簡單 進來判斷是否大於0,如果大於0 就建立一個傳進來大小的一個陣列, 如果為0就是空陣列
collection引數建構函式
public ArrayList(Collection<? extends E> c) {
// 指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排
// 這裡主要做了兩步:1.把集合的元素copy到elementData中。2.更新size值。
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData,size,Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
複製程式碼
把集合的元素重新放到新的集合裡面,然後更新實際容量的大小
具體方法
add方法
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 擴容操作,稍後講解
ensureCapacityInternal(size + 1); // Increments modCount!!
// 將元素新增到陣列尾部
elementData[size++] = e;
return true;
}
複製程式碼
第一步 擴容操作 第二步 往尾部新增一個元素 跟著往下看
ensureCapacityInternal(xxx); 確定內部容量的方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) { //看,判斷初始化的elementData是不是空的陣列,也就是沒有長度
//因為如果是空的話,minCapacity=size+1;其實就是等於1,空的陣列沒有長度就存放不了,所以就將minCapacity變成10,也就是預設大小,但是帶這裡,還沒有真正的初始化這個elementData的大小。
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//確認實際的容量,上面只是將minCapacity=10,這個方法就是真正的判斷elementData是否夠用
ensureExplicitCapacity(minCapacity);
}
複製程式碼
ensureExplicitCapacity(xxx);
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//minCapacity如果大於了實際elementData的長度,那麼就說明elementData陣列的長度不夠用,不夠用那麼就要增加elementData的length。這裡有的讀者就會模糊minCapacity到底是什麼呢,這裡給你們分析一下
/*第一種情況:由於elementData初始化時是空的陣列,那麼第一次add的時候,minCapacity=size+1;也就minCapacity=1,在上一個方法(確定內部容量ensureCapacityInternal)就會判斷出是空的陣列,就會給
  將minCapacity=10,到這一步為止,還沒有改變elementData的大小。
 第二種情況:elementData不是空的陣列了,那麼在add的時候,minCapacity=size+1;也就是minCapacity代表著elementData中增加之後的實際資料個數,拿著它判斷elementData的length是否夠用,如果length
不夠用,那麼肯定要擴大容量,不然增加的這個元素就會溢位。
*/
if (minCapacity - elementData.length > 0)
//arrayList能自動擴充套件大小的關鍵方法就在這裡了
grow(minCapacity);
}
複製程式碼
grow(xxx); arrayList核心的方法,能擴充套件陣列大小的真正祕密。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //將擴充前的elementData大小給oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
if (newCapacity - minCapacity < 0)//這句話就是適應於elementData就空陣列的時候,length=0,那麼oldCapacity=0,newCapacity=0,所以這個判斷成立,在這裡就是真正的初始化elementData的大小了,就是為10.前面的工作都是準備工作。
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超過了最大的容量限制,就呼叫hugeCapacity,也就是將能給的最大值給newCapacity
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size,so this is a win:
//新的容量大小已經確定好了,就copy陣列,改變容量大小咯。
elementData = Arrays.copyOf(elementData,newCapacity);
}
複製程式碼
hugeCapacity();
//這個就是上面用到的方法,很簡單,就是用來賦最大值。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果minCapacity都大於MAX_ARRAY_SIZE,那麼就Integer.MAX_VALUE返回,反之將MAX_ARRAY_SIZE返回。因為maxCapacity是三倍的minCapacity,可能擴充的太大了,就用minCapacity來判斷了。
//Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639 也就是說最大也就能給到第一個數值。還是超過了這個限制,就要溢位了。相當於arraylist給了兩層防護。
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
複製程式碼
總結一下擴容過程
- 1.判斷是否需要擴容,如果需要,計算需要擴容的最小容量
- 2.如果確定擴容,就執行grow(int minCapacity),minCapacity為最少需要的容量
- 3.第一次擴容是的容量大小是原來的1.5倍
- 4如果第一次 擴容後容量還是小於minCapacity,那就擴容為minCapacity
- 5.最後,如果minCapacity大於最大容量,則就擴容為最大容量
add(int,E)方法
public void add(int index,E element) {
// 插入陣列位置檢查
rangeCheckForAdd(index);
// 確保容量,如果需要擴容的話則自動擴容
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData,index,elementData,index + 1,size - index); // 將index後面的元素往後移一個位置
elementData[index] = element; // 在想要插入的位置插入元素
size++; // 元素大小加1
}
// 針對插入陣列的位置,進行越界檢查,不通過丟擲異常
// 必須在0-最後一個元素中間的位置插入,,否則就報錯
// 因為陣列是連續的空間,不存在斷開的情況
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製程式碼
這個也不難 前面我們陣列已經實現了
get方法
public E get(int index) {
// 先進行越界檢查
rangeCheck(index);
// 通過檢查則返回結果資料,否則就丟擲異常。
return elementData(index);
}
// 越界檢查的程式碼很簡單,就是判斷想要的索引有沒有超過當前陣列的最大容量
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
複製程式碼
先檢查陣列越界,然後你就是直接去陣列那邊拿資料然後返回
set(int index,E element)
// 作用:替換指定索引的元素
public E set(int index,E element) {
// 索引越界檢查
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
複製程式碼
這個就是替換指定位置的元素
remove(int):通過刪除指定位置上的元素
public E remove(int index) {
// 索引越界檢查
rangeCheck(index);
modCount++;
// 得到刪除位置元素值
E oldValue = elementData(index);
// 計算刪除元素後,元素右邊需要向左移動的元素個數
int numMoved = size - index - 1;
if (numMoved > 0)
// 進行移動操作,圖形過程大致類似於上面的add(int,E)
System.arraycopy(elementData,index+1,numMoved);
// 元素大小減1,並且將最後一個置為null.
// 置為null的原因,就是讓gc起作用,所以需要顯示置為null
elementData[--size] = null; // clear to let GC do its work
// 返回刪除的元素值
return oldValue;
}
複製程式碼
remove(Object o)
public boolean remove(Object o) {
// 如果刪除元素為null,則迴圈找到第一個null,並進行刪除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 根據下標刪除
fastRemove(index);
return true;
}
} else {
// 否則就找到陣列中和o相等的元素,返回下標進行刪除
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,numMoved);
// 元素大小減1,並且將最後一個置為null. 原因同上。
elementData[--size] = null; // clear to let GC do its work
}
複製程式碼
總結
ArrayList的優缺點
- ArrayList底層以陣列實現,是一種隨機訪問模式,再加上它實現了RandomAccess介面,因此查詢也就是get的時候非常快
- ArrayList在順序新增一個元素的時候非常方便,只是往陣列裡面添加了一個元素而已
- 刪除元素時,涉及到元素複製,如果要複製的元素很多,那麼就會比較耗費效能
- 插入元素時,涉及到元素複製,如果要複製的元素很多,那麼就會比較耗費效能
為什麼ArrayList的elementData是用transient修飾的
- 說明:ArrayList實現了Serializable介面,這意味著ArrayList是可以被序列化的,用transient修飾elementData意味著我不希望elementData陣列被序列化
- 理解:序列化ArrayList的時候,ArrayList裡面的elementData未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那麼是否有必要序列化整個elementData呢?顯然沒有這個必要,因此ArrayList中重寫了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();
}
}
複製程式碼
- 優點:這樣做既提高了序列化的效率,減少了無意義的序列化;而且還減少了序列化後檔案大小。
版本說明
- 這裡的原始碼是JDK8版本,不同版本可能會有所差異,但是基本原理都是一樣的。
結尾
ArrayList就這麼多了,大家自己可以對著部落格,對著原始碼看,我感激它這個原始碼不是很難,基於陣列的把可能是 因為博主也是一個開發萌新 我也是一邊學一邊寫 我有個目標就是一週 二到三篇 希望能堅持個一年吧 希望各位大佬多提意見,讓我多學習,一起進步。
日常求贊
好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才。
創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見
六脈神劍 | 文 【原創】如果本篇部落格有任何錯誤,請批評指教,不勝感激 !