一、定義
ArrayList 是一個數組隊列,相當于動态數組。與Java中的數組相比,它的容量能動态增長。它繼承于AbstractList類,實作了List, RandomAccess, Cloneable, java.io.Serializable這些接口。
要點:
1、繼承于AbstractList類,實作了List接口
2、實作RandomAccess接口,提供了随機通路功能。RandmoAccess是java中用來被List實作,為List提供快速通路功能。
3、ArrayList中的操作不是線程安全的!是以,建議在單線程中才使用ArrayList,而在多線程中可以選擇Vector或者CopyOnWriteArrayList(系列源碼後續講到)。
二、屬性
// 儲存ArrayList中資料的數組
private transient Object[] elementData;
// ArrayList中實際資料的數量
private int size;
要點:
1、elementData是個動态數組,我們能通過構造函數 ArrayList(int initialCapacity)來執行它的初始容量為initialCapacity;如果通過不含參數的構造函數ArrayList()來建立ArrayList,則elementData的容量預設是10。可進行動态擴容
2、有個關鍵字需要解釋:transient。Java的serialization提供了一種持久化對象執行個體的機制。當持久化對象時,可能有一個特殊的對象資料成員,我們不想用serialization機制來儲存它。為了在一個特定對象的一個域上關閉serialization,可以在這個域前加上關鍵字transient。簡單了解為就是:序列化該對象時,某個屬性不想被序列化,可加transient關鍵字避免。
3、注意size為動态數組的實際大小。
三、構造函數
// ArrayList帶容量大小的構造函數。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
// 建立一個數組
this.elementData = new Object[initialCapacity];
}
// ArrayList構造函數。預設容量是10。
public ArrayList() {
this(10);
}
// 構造一個包含指定元素的list,這些元素的是按照Collection的疊代器傳回的順序排列的
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 第一個構造方法使用提供的initialCapacity來初始化elementData數組的大小。
- 第二個構造方法調用第一個構造方法并傳入參數10,即預設elementData數組的大小為10。
- 第三個構造方法則将提供的集合轉成數組傳回給elementData(傳回若不是Object[]将調用Arrays.copyOf方法将其轉為Object[]),也很常用。
四、常見API
1、添加
在講添加元素之前,我們先來看一個函數:
/**
* 數組容量檢查,不夠時則進行擴容
*/
public void ensureCapacity( int minCapacity) {
//對于modCount變量,我們後面會給出解釋
modCount++;
// 目前數組的長度
int oldCapacity = elementData .length;
// 最小需要的容量大于目前數組的長度則進行擴容
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
// 新擴容的數組長度為舊容量的1.5倍+1
int newCapacity = (oldCapacity * 3)/2 + 1;
// 如果新擴容的數組長度還是比最小需要的容量小,則以最小需要的容量為長度進行擴容
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
// 進行資料拷貝,Arrays.copyOf底層實作是System.arrayCopy()
elementData = Arrays.copyOf( elementData, newCapacity);
}
}
當進行添加元素時,需要進行是否擴容判斷。當增加modCount之後,判斷minCapacity(即size+1)是否大于oldCapacity(即elementData.length),若大于,則調整容量為max((oldCapacity*3)/2+1,minCapacity)(将一個集合插入到list會出現後者,比如addAll),調整elementData容量為新的容量,即将傳回一個内容為原數組元素,大小為新容量的數組賦給elementData;否則不做操作。
注意:當需要擴容時,jdk1.6容量為max((oldCapacity*3)/2+1,minCapacity);
jdk1.7容量為max(oldCapacity + (oldCapacity >> 1),minCapacity) 。
關于添加的四個函數:
/**
* 添加一個元素
*/
public boolean add(E e) {
// 進行擴容檢查
ensureCapacity( size + 1); // Increments modCount
// 将e增加至list的資料尾部,容量+1
elementData[size ++] = e;
return true;
}
/**
* 在指定位置添加一個元素
*/
public void add(int index, E element) {
// 判斷索引是否越界,這裡會抛出多麼熟悉的異常。。。
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
// 進行擴容檢查
ensureCapacity( size+1); // Increments modCount
// 對數組進行複制處理,目的就是空出index的位置插入element,并将index後的元素位移一個位置,共有
//size-index個元素需要進行移動
System. arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将指定的index位置指派為element
elementData[index] = element;
// list容量+1
size++;
}
/**
* 增加一個集合元素
*/
public boolean addAll(Collection<? extends E> c) {
//将c轉換為數組
Object[] a = c.toArray();
int numNew = a.length ;
//擴容檢查
ensureCapacity( size + numNew); // Increments modCount
//将c添加至list的資料尾部
System. arraycopy(a, 0, elementData, size, numNew);
//更新目前容器大小
size += numNew;
return numNew != 0;
}
/**
* 在指定位置,增加一個集合元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length ;
ensureCapacity( size + numNew); // Increments modCount
// 計算需要移動的長度(index之後的元素個數)
int numMoved = size - index;
// 數組複制,空出第index到index+numNum的位置,即将數組index後的元素向右移動numNum個位置
if (numMoved > 0)
System. arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 将要插入的集合元素複制到數組空出的位置中
System. arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
2、删除
删除操作常見API如下:
/**
* 根據索引位置删除元素
*/
public E remove( int index) {
// 數組越界檢查
RangeCheck(index);
modCount++;
// 取出要删除位置的元素,供傳回使用
E oldValue = (E) elementData[index];
// 計算數組要複制的數量
int numMoved = size - index - 1;
// 數組複制,就是将index之後的元素往前移動一個位置
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将數組最後一個元素置空(因為删除了一個元素,然後index後面的元素都向前移動了,是以最後一個就沒用了),好讓gc盡快回收
// 不要忘了size減一
elementData[--size ] = null; // Let gc do its work
return oldValue;
}
/**
* 根據元素内容删除,隻删除比對的第一個
*/
public boolean remove(Object o) {
// 對要删除的元素進行null判斷
// 對資料元素進行周遊查找,知道找到第一個要删除的元素,删除後進行傳回,如果要删除的元素正好是最後一個那就慘了,時間複雜度可達O(n) 。。。
if (o == null) {
for (int index = 0; index < size; index++)
// null值要用==比較
if (elementData [index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 非null當然是用equals比較了
if (o.equals(elementData [index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
// 原理和之前的add一樣,還是進行數組複制,将index後的元素向前移動一個位置,不細解釋了,
int numMoved = size - index - 1;
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
//後面的元素交給垃圾回收器
elementData[--size ] = null; // Let gc do its work
}
/**
* 數組越界檢查
*/
private void RangeCheck(int index) {
if (index >= size )
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
}
上面講增加元素可能會進行擴容,而删除元素卻不會進行縮容,如果進行一次大擴容後,我們後續隻使用了幾個空間或者繼續進行删除操作?
/**
* 将底層數組的容量調整為目前實際元素的大小,來釋放空間。
*/
public void trimToSize() {
modCount++;
// 目前數組的容量
int oldCapacity = elementData .length;
// 如果目前實際元素大小 小于 目前數組的容量,則進行縮容
if (size < oldCapacity) {
elementData = Arrays.copyOf( elementData, size );
}
3、查詢/更新太簡單,就不貼上了。
4、是否包含:
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//找到指定對象的第一個索引位置,從前往後,判斷對象是否為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;
}
//與前面相反,從後往前
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData [i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData [i]))
return i;
}
return -1;
}
五、Fast-Fail快速失敗機制
此類的
iterator
和
listIterator
方法傳回的疊代器是快速失敗的:在建立疊代器之後,除了通過疊代器自身的
remove
或
add
方法從結構上對清單進行修改,否則在任何時間以任何方式對清單進行修改,疊代器都會抛出
ConcurrentModificationException
。是以,面對并發的修改,疊代器很快就會完全失敗,而不是冒着在将來某個不确定時間發生任意不确定行為的風險。
注意,疊代器的快速失敗行為無法得到保證,快速失敗疊代器會盡最大努力抛出
ConcurrentModificationException
。疊代器的快速失敗行為應該僅用于檢測 bug。
前面說到
ArrayList
中定義了一個
modCount
來記錄對容器進行結構修改的次數,在
add
、
addAll
、
remove
、
clear
、
clone
方法中都會引起
modCount
變化,而在建立疊代器時,會使用局部變量儲存目前的
modCount
值:
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;
...
在進行疊代的過程中,會先檢查
modCount
有沒有發生變化,以此來判定是否有外部操作改變了容器:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
最後,因為
ArrayList
是非同步的,是以,在多線程環境下,如果有對容器進行結構修改的操作,則必須使用外部同步。
舉個例子:移除ArrayList某一個指定元素:
import java.util.ArrayList;
public class ArrayListRemove
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
list.add("c");
list.add("c");
remove(list);
for (String s : list)
{
System.out.println("element : " + s);
}
}
public static void remove(ArrayList<String> list)
{
// TODO:
}
}
如果要想删除list的b字元,有下面兩種常見的錯誤例子:
錯誤寫法執行個體一:
public static void remove(ArrayList<String> list)
{
for (int i = 0; i < list.size(); i++)
{
String s = list.get(i);
if (s.equals("b"))
{
list.remove(s);
}
}
}
錯誤的原因:這種最普通的循環寫法執行後會發現第二個“b”的字元串沒有删掉。
錯誤寫法執行個體二:
public static void remove(ArrayList<String> list)
{
for (String s : list)
{
if (s.equals("b"))
{
list.remove(s);
}
}
}
錯誤的原因:這種for-each寫法會報出著名的并發修改異常:java.util.ConcurrentModificationException。
先解釋一下執行個體一的錯誤原因。翻開JDK的ArrayList源碼,先看下ArrayList中的remove方法(注意ArrayList中的remove有兩個同名方法,隻是入參不同,這裡看的是入參為Object的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;
}
一般情況下程式的執行路徑會走到else路徑下最終調用faseRemove方法:
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; // Let gc do its work
}
可以看到會執行System.arraycopy方法,導緻删除元素時涉及到數組元素的移動。針對錯誤寫法一,在周遊第一個字元串b時因為符合删除條件,是以将該元素從數組中删除,并且将後一個元素移動(也就是第二個字元串b)至目前位置,導緻下一次循環周遊時後一個字元串b并沒有周遊到,是以無法删除。針對這種情況可以倒序删除的方式來避免:
public static void remove(ArrayList<String> list)
{
for (int i = list.size() - 1; i >= 0; i--)
{
String s = list.get(i);
if (s.equals("b"))
{
list.remove(s);
}
}
}
因為數組倒序周遊時即使發生元素删除也不影響後序元素周遊。
接着解釋一下執行個體二的錯誤原因。錯誤二産生的原因卻是foreach寫法是對實際的Iterable、hasNext、next方法的簡寫,問題同樣處在上文的fastRemove方法中,可以看到第一行把modCount變量的值加一,但在ArrayList傳回的疊代器(該代碼在其父類AbstractList中):
public Iterator<E> iterator() {
return new Itr();
}
這裡傳回的是AbstractList類内部的疊代器實作private class Itr implements Iterator,看這個類的next方法:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
第一行checkForComodification方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
這裡會做疊代器内部修改次數檢查,因為上面的remove(Object)方法修改了modCount的值,是以才會報出并發修改異常。要避免這種情況的出現則在使用疊代器疊代時(顯示或for-each的隐式)不要使用ArrayList的remove,改為用Iterator的remove即可。
public static void remove(ArrayList<String> list)
{
Iterator<String> it = list.iterator();
while (it.hasNext())
{
String s = it.next();
if (s.equals("b"))
{
it.remove();
}
}
}
總結:
在需要擴容的時候,調用Arrays.copyOf(),這個是建立新數組,底層也是System.arraycopy()
其他操作時用的是System.arraycopy(),直接在原數組上操作。