相關文章 Java集合框架分析(一)綜合概述java
本篇文章主要分析一下 Java 集合框架中的 List 部分,ArrayList,該源碼分析基於JDK1.8,分析工具,AndroidStudio,文章分析不足之處,還請指正!數組
ArrayList 底層維護的是一個動態數組,每一個 ArrayList 實例都有一個容量。該容量是指用來存儲列表元素的數組的大小。它老是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增加。ArrayList 不是同步的(也就是說不是線程安全的,同 HashMap、LinkedHashMap 同樣),若是多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步,在多線程環境下,可使用 Collections.synchronizedList 方法聲明一個線程安全的 ArrayList,例如:安全
List arraylist = Collections.synchronizedList(new ArrayList());
複製代碼
首先咱們來看一下關於ArrayList的類結構,bash
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製代碼
上面是是 ArrayList 類的定義,它繼承了抽象類 AbstractList,可是真正繼承的方法只有 equals 和 hashCode,別的方法在 ArrayList 中都有本身的從新實現;List 接口在 AbstractList 中已經實現,這裏是爲了代表一下,沒有太大含義;RandomAccess 沒有方法,代表 ArrayList 支持快速隨機訪問;實 現了 Cloneable 接口,以指示 Object.clone() 方法能夠合法地對該類實例進行按字段複製,若是在沒有實現 Cloneable 接口的實例上調用 Object 的 clone 方法,則會致使拋出 CloneNotSupportedException 異常;類經過實現 java.io.Serializable 接口以啓用其序列化功能,未實現此接口的類將沒法使其任何狀態序列化或反序列化多線程
接着咱們分析一下 ArrayList 定義的變量。框架
//序列化ID
private static final long serialVersionUID = 8683452581122892189L;
//數組初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//
private static final Object[] EMPTY_ELEMENTDATA = {};
//elementData存儲ArrayList內的元素
transient Object[] elementData;
//存儲在ArrayList內的元素的數量
private int size;
複製代碼
//設定初始容量大小的構造函數
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//數組初始化
this.elementData = new Object[initialCapacity];
}
//無參數構造函數
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
//將提供的集合轉成數組返回給elementData(返回若不是Object[]將調用Arrays.copyOf方法將其轉爲Object[])
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
複製代碼
構造函數仍是比較簡單的,咱們來看看 ArrayList 的最經常使用的方法 add,dom
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
複製代碼
發現 ArrayList 的 add 方法,只有簡單的兩行代碼,粗略的看一下是在數組 elementData 的尾部添加一個元素 E,那麼究竟是怎麼樣操做的呢?咱們詳細查看一下源碼,首先進入 ensureCapacityInternal 方法中一探究竟。看這個方法的名字就大概知道這是確保數組容量的,怎麼個確保法呢?函數
private void ensureCapacityInternal(int minCapacity) {
//當咱們調用ArrayList的無參構造函數時調用此代碼
if (elementData == EMPTY_ELEMENTDATA) {
//設置數組容量爲10,默認的大小
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//
ensureExplicitCapacity(minCapacity);
}
//確保容量大小,modCount自增,並判斷數組大小是否足夠,不夠的話將增大數組
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//若是最小須要空間比elementData的內存空間要大,則須要擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
複製代碼
到了這裏仍是有點模糊到底怎麼數組擴容的?咱們接着看 grow 的方法工具
//擴容並從新copy一份數組
private void grow(int minCapacity) {
// oldCapacity爲當前數組大小
int oldCapacity = elementData.length;
// newCapacity爲新容量的大小等於舊容量的1.5倍大小
int newCapacity = oldCapacity + (oldCapacity >> 1);
//若是擴充後的容量仍是比最少須要的容量還要小的話,就設置擴充容量爲最小須要的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//判斷最新的容量是否已經超出最大數組範圍,溢出判斷
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 調用Arrays.copyOf方法將elementData數組指向新的內存空間時newCapacity的連續空間
// 並將elementData的數據複製到新的內存空間
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
咱們總結一下 add 操做的過程,首先咱們先要判斷是否須要擴容,在擴容判斷裏面,咱們主要進行一些判斷,若是當前所須要的容量和當前數組的容量進行比較,若是不夠的話則進行擴容,在擴容的同時則須要判斷是否溢出了,而後將舊的數據數組進行 copy 到新的容量大小的數組中,最後再將須要添加的數據添加到數組的最後一位。源碼分析
咱們接着分析 add 的另外一個方法,重載 add(int index, E element) 方法,
//在指定的位置上面插入一個數據
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//判斷是否須要擴容
ensureCapacityInternal(size + 1); // Increments modCount!!
//須要將指定位置及其後面數據向後移動一位,而後在位置上面插入數據
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//插入數據
elementData[index] = element;
//大小改變
size++;
}
複製代碼
中心思想就是,首先判斷指定位置 index 是否超出 elementData 的界限,以後調用 ensureCapacity 調整容量(若容量足夠則不會拓展),調用 System.arraycopy 將 elementData 從 index 開始的 size-index 個元素複製到 index+1 至 size+1 的位置(即 index 開始的元素都向後移動一個位置),而後將 index 位置的值指向 element,同時改變數組的大小。
接着咱們分析一下 addAll 方法。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
//將a的第0位開始拷貝至elementData的size位開始,拷貝長度爲numNew
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
複製代碼
其實這個方法很好理解,首先將批量添加的數據轉爲數組,而後獲取它的容量大小,判斷一下,若是咱們要在數組中插入這一批數據的話,是否須要擴容,通過這個擴容判斷操做,咱們的數組容量是足夠了,而後咱們開始批量導入數據,其中 System.arraycopy(a, 0, elementData, size, numNew) ;意思就是,將須要導入的批數據從 0 開始,導入到數組從 size 位置開始,導入的大小數量就是須要導入的數量。這個時候,總的數組容量就變成了原先的容量加上導入的容量了。
addAll 還有一個重載方法,咱們也來看看:
public boolean addAll(int index, Collection<? extends E> c) {
//判斷插入的位置是否越界
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
複製代碼
這個方法其實在 add 的方法基礎上面修改而來,複製數組的時候主要進行兩步複製,第一步,現將原數組在指定的位置上面向後移動須要的位數,而後第二步再將須要導進去的數據從 index 位置複製進去。
分析完了 add 方法,咱們來分析分析 get 方法內容,get 方法也是比較簡單的
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
複製代碼
首先判斷須要返回的位置是否越界了,其次直接返回數組對應索引裏面的值。很是簡單,咱們再看看其餘方法。
咱們來看看 remove 方法
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
//保存一下須要刪除的數據
E oldValue = (E) 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;
}
複製代碼
同理,由於涉及到數組的問題,因此首先須要判斷一下是否出現越界的問題,而後就開始將指定刪除位置後面的數據都向前移動一位,而後將最後的一位設置爲 null,最後返回刪除的數據。
刪除一個數據,remove 也有重載方法
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;
}
複製代碼
刪除一個數據,主要是遍歷數組,看看數組中是否存在這個數據,若是存在的話則進行fastRemove,咱們進入 fastRemove 中看看
//刪除指定位置上面的數據
private void fastRemove(int index) {
modCount++;
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
}
複製代碼
將指定位置 index 後面的數據所有向前移動一位,最後將最後一位回收掉。
另外咱們看下所有刪除數組中的數據方法 clear
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
複製代碼
將數組中全部的數據都重置爲 null,等帶回收。
protected void removeRange(int fromIndex, int toIndex) {
if (toIndex < fromIndex) {
throw new IndexOutOfBoundsException("toIndex < fromIndex");
}
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
複製代碼
執行過程是將 elementData 從 toIndex 位置開始的元素向前移動到 fromIndex,而後將 toIndex 位置以後的元素所有置空順便修改 size。
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
複製代碼
這個方法很簡單,就是修改數組 index 裏面的數據,並返回舊的數據。
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;
}
複製代碼
返回在數組中第一次出現的值。若是找到這個值的話就返回 index,找不到就返回-1。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}
複製代碼
因爲 elementData 的長度會被拓展,size 標記的是其中包含的元素的個數。因此會出現 size 很小但 elementData.length 很大的狀況,將出現空間的浪費。trimToSize 將返回一個新的數組給elementData,元素內容保持不變,length 跟 size 相同,節省空間。
以上即是 ArrayList 的源碼內容,總的來講不是很難,接下來咱們來總結一下關於 ArrayList 的方方面面。
ArrayList 底層是基於數組來實現的,能夠經過下標準確的找到目標元素,所以查找的效率高;可是添加或刪除元素會涉及到大量元素的位置移動,效率低。
ArrayList 提供了三種不一樣的構造方法,無參數的構造方法默認在底層生成一個長度爲 10 的 Object 類型的數組,當集合中添加的元素個數大於 10,數組會自動進行擴容,即生成一個新的數組,並將原數組的元素放到新數組中。
ensureCapacity 方法對數組進行擴容,它會生成一個新數組,長度是原數組的 1.5 倍 +1,隨着向 ArrayList 中不斷添加元素,當數組長度沒法知足須要時,重複該過程。
ArrayList 不是同步的(也就是說不是線程安全的,同 HashMap、LinkedHashMap 同樣),若是多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步,在多線程環境下,可使用 Collections.synchronizedList 方法聲明一個線程安全的 ArrayList,例如:List arraylist = Collections.synchronizedList(new ArrayList());
專一於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎你們關注,一塊兒交流學習~