1、前言
說到ArrayList,就不得不說Array。光看名字,還以為這2個是同一個東西。其實不然。
Array:指容量為固定的數組,常見的初始化方法如下:
String[] names = {"david","tom","kate"};
在聲明的時候直接聲明了內部元素,這樣jvm就可以快速的分配給指定大小的空間。同時,看Array源碼可知,Array的方法基本上都是native方法,其底層實現均為c/c++實現。
ArrayList:動態數組,允許新增、刪除等操作,常見的初始化方法:
List<String> nameList = new ArrayList<>();
看完概念,我們開始正題,開始源碼的解讀。
2、與其他容器對比
與hashmap不同,ArrayList為集合的一種,來源自數據結構中的數組概念,與hashmap源自數據結構中的圖概念不同。
我們先看ArrayList的uml圖。
可以看出,ArrayList不僅實現了Cloneable、Serializable接口,還 實現了RandomAccess接口、List接口。
3、簡述RandomAccess接口
在后續代碼中,還可以看到RandomAccess這個接口。若實現了該接口,則表明該類可以進行下標式訪問,類似于這樣:
List<String> nameList = new ArrayList<>();
String david = names[0];
(這個會提示越界了)
4、成員變量分析
ArrayList的成員變量分析如下:
//默認容量
private static final int DEFAULT_CAPACITY = 10;
//空數組
private static final Object[] EMPTY_ELEMENTDATA = {};
//也是空數組,作用是在新增元素的時候用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//數組
transient Object[] elementData;
//數組大小
private int size;
//數組最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//繼承自AbstractList,用于fail-fast機制
protected transient int modCount = 0;
從成員變量來看,數組用transient修飾符修飾,作用應該是類似于hashmap的Node[]。
5、核心方法分析
5.1 容量擴增
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新空間分配直接擴大50%
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);
}
private static int hugeCapacity(int minCapacity) {
//這一點比較特殊,見下面分析
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
關于hugeCapacity的判斷小于0則為溢出,由于在jvm內部是以反碼存儲的數據,首位為符號位,當容量擴增后,若溢出,首位則變為1,此時變為負數,則可以快速判斷出是否溢出。
5.2 trimToSize 壓縮空間
去掉多余的空對象,精簡存儲空間
public void trimToSize() {
modCount++;
//代碼也寫的很簡潔,經常使用三元表達式
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
由上面的擴增容量可知,如果原始容量是100,在擴增容量后,那么分配的容量為150.但是實際上可能只存110個對象實例,那么此時調用這個方法,就可以節約一定的存儲空間。不過若數組較大,那么操作可能會耗費一點時間。
5.3 fail-fast機制
這個其實在hashmap中也采取了類似機制,就是額外有一個成員變量,用于快速判斷該實例是否有變化,若在進行迭代的時候有變更,那么就拋出一個并發修改異常(ConcurrentModificationException)。
5.4 indexOf 求 下標
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;
}
這段代碼略過,用的比較多,沒什么亮點。
5.5 新增一個元素
既然是數組,那么就有兩種 新增的方式:指定特定的位置、向后插入。
向指定位置寫入元素:
public void add(int index, E element) {
//下標檢查,是否越界了
rangeCheckForAdd(index);
//擴增容量,同時改變modcount
ensureCapacityInternal(size + 1); // Increments modCount!!
//index后面的元素后移
System.arraycopy(elementData, index, elementData, index + 1, size - index);
//指定位置放置元素
elementData[index] = element;
//元素數量大小自增
size++;
}
向后插入一個元素:
public boolean add(E e) {
//擴大容量,修改modcount
ensureCapacityInternal(size + 1); // Increments modCount!!
//注意
//數組是從0開始的存元素的,而數組個數是從1開始計數的
//這個地方是往第size個位置上存元素
//再將元素個數加1
elementData[size++] = e;
return true;
}
從以上源碼可以看出,向后插入一個元素不用進行元素的復制,自然效率要大于指定位置插入一個元素。
5.6 移除一個元素
同新增,也是分2種。
移除指定位置的元素:
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; // clear to let GC do its work
return oldValue;
}
刪除指定元素:
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);
elementData[--size] = null; // clear to let GC do its work
}
看到這個指定刪除某個元素的代碼,那么有人就會有疑問了,如果我里面有多個一樣的元素,那這不是刪不完嗎?這個時候你得使用removeAll:
public boolean removeAll(Collection<?> c) {
//參數校驗,僅判斷c != null,感覺不太嚴謹
Objects.requireNonNull(c);
return batchRemove(c, false);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
//常量數組,不允許再賦值,但是數組內部的元素允許自由移動、被重新賦值
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
//不包含該元素,則存到新數組,其實是位置前移
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//上面if內類型不匹配拋異常時,r與size不等
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
//由于將已包含的元素前移了,那么不包含的元素都在后面,直接將后面的元素刪掉
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
5.7 序列化
這個源碼類似于hashmap,就不重復分析了。
5.8 其他
set、get方法比較簡單, 就不分析了。
需要注意的是,arraylist繼承的AbstractList覆寫了hashcode和equals方法。
6 小結
總的來說,arraylist的源碼比較簡單,可供分析的內容不多。