數組
- 簡單:數組是一種最簡單的數據結構
- 占據連續內存:數組空間連續,按照申請的順序存儲,但是必須制定數組大小
- 數組空間效率低:數組中經常有空閑的區域沒有得到充分的應用
- 操作麻煩:數組的增加和刪除操作很麻煩
ArrayList 應用場景
優點:尾插效率高,支持隨機訪問。
缺點:中間插入或者刪除效率低。
排序不要使用 ArrayList,經常增刪改變位置不要用 ArrayList,效率很低。
只存放,隨機訪問,可以使用 ArrayList。
ArrayList 順序刪除節點要用迭代器從尾部往前刪,如果從前往后刪的話,會頻繁觸發 System.arraycopy。
遍歷 ArrayList 盡量使用迭代器。
優化ArrayList
如果我們要在集合中添加一百萬
個數據,它只能是通過每次擴容1.5倍
,每次將原數組數據放入新的數組,很明顯非常消耗資源。
構造方法
在構造ArrayList時傳入一個整型,就會直接構造一個該長度的集合,避免一直擴容造成的資源浪費。
使用普通方法
還可以使用ensureCapacity(int minCapacity)
方法。參數為擴容目標大小。
如果要加入大量的數據,這種方式可以比傳統方式快10倍以上,加入數據越多,越明顯。
線性表
為了應對數組的缺點,可以使用線性表。
按物理結構劃分:順序存儲結構(順序表)、鏈式存儲結構。
順序表(ArrayList)
a1 是 a2 的前驅,ai+1 是 ai 的后繼,a1 沒有前驅,an 沒有后繼。
n 為線性表的長度 ,若 n==0 時,線性表為空表。
它繼承于AbstractList,實現了List、RandomAccess、Cloneable、 Serializable等接口。
ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent并發包下的CopyOnWriteArrayList類。
ArrayList實現了RandomAccess接口,即提供了隨機訪問功能。RandomAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們可以通過元素的序號快速獲取元素對象,這就是快速隨機訪問;實現了Cloneable接口,能被克隆;實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸。
封裝了數組:
public class ArrayList<E> extends AbstractList<E> implements List<E>,
RandomAccess, Cloneable, java.io.Serializable {
// transient是短暫的意思。對于transient 修飾的成員變量,
// 在類的實例對象的序列化處理過程中會被忽略。
// 因此,transient變量不會貫穿對象的序列化和反序列化,
// 生命周期僅存于調用者的內存中而不會寫到磁盤里進行持久化。
transient Object[] elementData;
private int size;
}
ArrayList源碼詳解
ArrayList內部通過一個Object數組來存儲數據:
transient Object[] elementData;
ArrayList使用size變量來表示實際存儲的元素個數:
private int size;
ArrayList有以下三個構造方法:
// 根據initialCapacity來創建具有指定初始容量的ArrayList
public ArrayList(int initialCapacity)
// 創建一個默認的ArrayList
public ArrayList()
// 根據其他集合來創建ArrayList
public ArrayList(Collection<? extends E> c)
詳細看一下這三個構造方法:
public ArrayList(int initialCapacity) {
// 創建指定初始容量的ArrayList
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
}
// 初始化容量指定為0,則用EMPTY_ELEMENTDATA數組
else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}
// 否則,拋出IllegalArgumentException異常
else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
EMPTY_ELEMENTDATA定義如下(即長度為0的Object數組):
private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
// 默認ArrayList的內部數組是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA聲明如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA是一樣的,都是定義為了長度為0的Object數組,那它們有什么區別呢?它們兩個的主要區別在于添加第一個元素時,若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,則程序會將其擴充為容量為DEFAULT_CAPACITY的數組,DEFAULT_CAPACITY定義為10,即通過默認的構造方法創建的ArrayList的初始容量是10。我們后面會詳細介紹數組的擴容。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 通過反射獲取數組類型,判定c.toArray類型是否為Object[]類型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 若c為空,則內部數組為EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
add方法
ArrayList有兩個重載的Add方法:
// 在數組elementData尾部添加一個元素
public boolean add(E e)
// 在數組elementData指定位置index處添加元素
public void add(int index, E element)
add(E e)方法
我們先來看add(E e)方法,源碼如下:
// 在數組elementData尾部添加一個元素
public boolean add(E e) {
// 容量大小判斷
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
該方法首先要判斷elementData數組的容量是否能夠容納新的元素,若不能,則需要進行擴容操作,然后將元素e放置在數組的size位置。ensureCapacityInternal(int)方法源碼如下:
private void ensureCapacityInternal(int minCapacity) {
// 若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// minCapacity = max(10, minCapacity)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 針對數組最小容量,決定是否擴容
ensureExplicitCapacity(minCapacity);
}
前面講到的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,在這里就起到作用了,若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,則會將數組的最小容量設置為10。然后通過ensureExplicitCapacity(int)方法來判斷是否要擴容:
private void ensureExplicitCapacity(int minCapacity) {
// 增加修改次數
modCount++;
// overflow-conscious code
// 增加元素后,ArrayList中要存儲的元素個數為minCapacity
// 若此時minCapacity > elementData原始的容量,則要按照minCapacity進行擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
擴容的最終操作是通過grow(int)方法來實現的:
private void grow(int minCapacity) {
// overflow-conscious code
// 獲取elementData的原始容量
int oldCapacity = elementData.length;
// 計算新的容量
// 若原數組長度為偶數,那么新數組長度就恰好是原數組長度的1.5倍
// 若原數組長度為奇數,那么新數組長度就恰好是原數組長度的1.5倍 - 1
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若按照1.5倍進行擴容后,capacity仍然比實際需要的小,則新容量更改為實際需要的大小,即minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新數組的長度比虛擬機能夠提供給數組的最大存儲空間大,則將新數組長度更改為最大正數:Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 按照新的容量newCapacity創建一個新數組,然后再將原數組中的內容copy到新數組中
elementData = Arrays.copyOf(elementData, newCapacity);
}
擴容函數整體比較好理解,需要注意的是,若新容量過大,則會通過hugeCapacity(int)方法來進行容量判斷:
private static int hugeCapacity(int minCapacity) {
// minCapacity < 0則表明數組容量已經超過了虛擬機所能表示的最大容量,拋出OutOfMemoryError
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 否則,若minCapacity > MAX_ARRAY_SIZE,則數組容量為Integer.MAX_VALUE,否則為MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
add(int index, E element)方法
add(int index, E element)方法源碼如下:
public void add(int index, E element) {
// 判斷下標index的合法性
rangeCheckForAdd(index);
// 數組容量判斷
ensureCapacityInternal(size + 1); // Increments modCount!!
// 數組拷貝,將index到末尾的元素拷貝到index + 1到末尾的位置,將index的位置留出來
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
該方法與add(E e)方法類似,只是元素的插入位置不同,該方法需要調用rangeCheckForAdd(int)方法來對index進行合法檢驗:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
若index下標不合法,則拋出IndexOutOfBoundsException異常。
remove方法
remove方法在ArrayList中同樣有兩種實現方式:
// 根據index下標刪除元素
public E remove(int index)
// 根據元素刪除
public boolean remove(Object o)
remove(int index)方法
remove(int index)方法源碼如下:
public E remove(int index) {
// 下標合法性檢驗
rangeCheck(index);
// 修改次數加1
modCount++;
// 獲取舊的元素值
E oldValue = elementData(index);
// 計算需要移動的元素個數
int numMoved = size - index - 1;
// 將元素向前移動
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將最后的元素值設置為null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
這里需要注意一點的就是rangeCheck(int)方法:
private void rangeCheck(int index) {
// 若index下標超出size,則拋出IndexOutOfBoundsException異常
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
這里只判斷了index超出了size,而不需要判斷index為負數的情況,這是為什么呢?
因為該方法總是在訪問數組之前被調用,在訪問數組時,會對下標為負數進行判斷,如果index為負數,則會拋出ArrayIndexOutOfBoundsException異常,所以在這里就沒有必要判斷了,避免冗余。
remove(Object o)方法
remove(Object o)方法源碼如下:
public boolean remove(Object o) {
// 若刪除的元素為null
if (o == null) {
for (int index = 0; index < size; index++)
// 若數組元素為null,則調用fastRemove方法快速刪除
if (elementData[index] == null) {
fastRemove(index);
return true;
}
}
// 若刪除的元素不為null
else {
for (int index = 0; index < size; index++)
// 找到要刪除的元素,調用fastRemove方法快速刪除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
ArrayList刪除元素時,是分為元素為null和不為null兩種方式來判斷的,這也說明ArrayList允許添加null元素;同時,如果這個元素在ArrayList中存在多個,則只會刪除最先出現的那個。
刪除元素,采用了fastRemove(int)方法來快速刪除:
private void fastRemove(int index) {
// 修改次數加1
modCount++;
// 計算需要移動的元素數目
int numMoved = size - index - 1;
// 將index之后的元素向前移動一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將數組最后一位置為null
elementData[--size] = null; // clear to let GC do its work
}
其他相關方法介紹
trimToSize()
trimToSize()源碼如下:
public void trimToSize() {
// 修改次數加1
modCount++;
// trim
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
該方法的主要工作就是將數組容量修改為size大小,若size為0,則將數組設置為EMPTY_ELEMENTDATA,否則,通過Arrays.copyOf方法來創建新的數組。
該方法的主要存在意義就是:如果capacity被分配過大,那么可以通過這個方法,將ArrayList實例的capacity的大小修改為數組存儲元素的個數,從而縮減ArrayList的存儲空間。
contains(Object o)
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
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;
}
toArray()
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
該方法有可能會拋出java.lang.ClassCastException異常,如果直接用向下轉型的方法,將整個ArrayList集合轉變為指定類型的Array數組,便會拋出該異常,而如果轉化為Array數組時不向下轉型,而是將每個元素向下轉型,則不會拋出該異常,顯然對數組中的元素一個個進行向下轉型,效率不高,且不太方便。
toArray(T[] a)
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
該方法可以直接將ArrayList轉換得到的Array進行整體向下轉型(轉型其實是在該方法的源碼中實現的),且從該方法的源碼中可以看出,參數a的大小不足時,內部會調用Arrays.copyOf方法,該方法內部創建一個新的數組返回,因此對該方法的常用形式如下:
public static Integer[] toArray(ArrayList<Integer> v) {
Integer[] array = (Integer[])v.toArray(new Integer[0]);
return array;
}
Arrays.copyOf()、System.arraycopy()
ArrayList的源碼中大量地調用了Arrays.copyof()和System.arraycopy()方法,我們下面深入詳解一下這兩個方法:
ArrayList中用的比較多的Arrays.copyOf()方法定義如下:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
該方法調用了其重載方法:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
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;
}
該方法實際上是在其內部又創建了一個長度為newlength的數組,調用System.arraycopy()方法,將原來數組中的元素復制到了新的數組中,下面來看System.arraycopy()方法:
public static native void arraycopy(Object src,
int srcPos,
Object dest, int destPos,
int length);
該方法被標記了native,調用了系統的C/C++代碼,在JDK中是看不到的,但在openJDK中可以看到其源碼:
static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
// Do better than this: inline memmove body NEEDS CLEANUP
if (from > to) {
while (count-- > 0) {
// Copy forwards
*to++ = *from++;
}
} else {
from += count - 1;
to += count - 1;
while (count-- > 0) {
// Copy backwards
*to-- = *from--;
}
}
}
JVM源碼主要思想就是,創建一個新的數組,然后通過上述方法將原數組的數據移動到新數組中。從注釋中可以看到,這種實現方式要優于C語言的memmove()方法,因為memmove()方法還需要進行內存清理工作。
該方法可以保證同一個數組內元素的正確復制和移動,比一般的復制方法的實現效率要高很多,很適合用來批量處理數組。Java強烈推薦在復制大量數組元素時用該方法,以取得更高的效率。
fail-fast機制
在ArrayList的源碼中,我們經常會看到modCount++這樣的代碼,其實,modCount是用來實現fail-fast機制的,fail-fast機制是Java集合中的一種錯誤機制,當多個線程對同一個集合的內容進行操作時,就會發生fail-fast事件,它是一種錯誤檢測機制,只能被用來檢測錯誤,因為JDK并不一定保證fail-fast機制一定會發生。fail-fast機制會盡最大努力來拋出ConcurrentModificationException異常。
fail-fast機制產生的最初原因是在于程序在對Collection進行迭代時,某個線程對該Collection的結構進行了修改。這時迭代器會拋出ConcurrentModificationException異常,從而產生fail-fast事件。如果單線程違法了規則,也同樣會拋出此異常。
迭代器在調用next()、remove()等方法時都要調用checkForComodification()方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
該方法主要是檢測modCount是否等于expectedModCount,若不等于,則拋出ConcurrentModificationException異常。
在創建迭代器時,會將modCount的值賦給expectedModCount,所以在迭代期間,expectedModCount不會改變,在ArrayList中,無論add、remove還是clear方法,只要改變了ArrayList的元素個數,都會導致modCount改變,從而可能導致fail-fast產生。
fail-fast解決方案
1、在遍歷過程中,所有涉及到改變modCount的地方全部加上synchronized或直接使用Collections.SynchronizedList。但不推薦該方案,因為增刪產生的同步鎖可能會阻塞遍歷操作。
2、使用CopyOnWriteArrayList來替換ArrayList,比較推薦該方案。
CopyOnWriteArrayList是 ArrayList的一個線程安全的變體,其中所有可變操作(add、remove等)都是通過對底層數組的一次復制來進行操作的,在以下情況很適用:
- 在不能或不想進行同步遍歷,但是又需要從并發中消除沖突時;
- 遍歷操作的數量大大超過了可變操作的數量,即讀多寫少時。
CopyOnWriteArrayList在copy的數組上進行修改,這樣就不會影響原數組中的數據,修改完之后,改變原有數據的引用即可。
對CopyOnWriteArrayList采用了一種讀寫分離的思想,對CopyOnWriteArrayList進行讀取操作不需要加鎖。但它存在以下缺點:
- 因為要復制一份底層數組,所以內存占用比較多;
- CopyOnWriteArrayList只能保證數據的最終一致性,不能保證數據的實時一致性。
所以,編寫程序時,要進行權衡利弊來選擇合適的數據結構。
原文鏈接:Java集合之ArrayList詳解_DivineH的博客-CSDN博客
參考:Java 線性表——ArrayList & LinkedList - 少元 - 博客園 (cnblogs.com)