1,前言
很久沒有寫博客了,很想念大家,18年都快過完了,才開始寫第一篇,爭取后面每周寫點,權當是記錄,因為最近在看JDK的Collection,而且ArrayList源碼這一塊也經常被面試官問道,所以今天也就和大家一起來總結一下
2,源碼解讀
當我們一般提到ArrayList的話都會脫口而出它的幾個特點:有序、可重復、查找速度快,但是插入和刪除比較慢,線程不安全,那么現在阿呆哥哥就會有這些疑問:為什么說是有序的?怎么有序?為什么又說插入和刪除比較慢?為什么慢?還有線程為什么不安全?所以帶着這些問題,我們一一的來源碼中來找找答案。
一般對於一個陌生的類,我們想使用它,都會先看它構造方法,再看它的屬性和方法,那么我們也按照這種方式來讀讀ArrayList這個類
2.1構造方法
ArrayList<String> arrayList = new ArrayList(); ArrayList<String> arrayList1 = new ArrayList(2);
一般來說我們常見使用ArrayList的創建方式是上面的這兩種
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final Object[] EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
上面是我們兩個構造方法和我們類中基本的屬性,從上面的代碼上來看,在創建構造基本上都沒有做,且定義了兩個默認的空數組,默認容器的大小DEFAULT_CAPACITY為10,還有我們真正存儲元素的地方elementData數組,所以這就是為什么說ArrayList存儲集合元素的底層時是使用數組來實現,OK,上面的代碼除了一個transient 修飾符之外我們同學們可能有點陌生之外,其余的應該都能看的懂,transient 有什么作用還有為什么用它修飾elementData字段,這個需要看完整個源碼之后,我再來給大家解釋的話比較合適,這里只需要留心一下。
還有一個不常用的構造方法
public ArrayList(Collection<? extends E> c) {
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;
}
}
第2行:利用Collection.toArray()方法得到一個對象數組,並賦值給elementData
第3行:size代表集合的大小,當通過別的集合來構造ArrayList的時候,需要賦值size
第5-6行:判斷 c.toArray()是否出錯返回的結果是否出錯,如果出錯了就利用Arrays.copyOf 來復制集合c中的元素到elementData數組中
第9行:如果c中元素數量為空,則將EMPTY_ELEMENTDATA空數組賦值給elementData
上面就是所有的構造函數的代碼了,這里我們可以看到,當構造函數走完之后,會創建出數組elementData和初始化size,Collection.toArray()則是將Collection中所有元素賦值到一個數組,Arrays.copyOf()則是根據Class類型來決定是new還是反射來創造對象並放置到新的數組中,源碼如下:
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;
}
這里面System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 這個方法在我們的后面會的代碼中會出現,就先講了,定義是:將數組src從下標為srcPos開始拷貝,一直拷貝length個元素到dest數組中,在dest數組中從destPos開始加入先的srcPos數組元素。相當於將src集合中的[srcPos,srcPos+length]這些元素添加到集合dest中去,起始位置為destPos
2.2 增加元素方法
一般經常使用的是下面三種方法
arrayList.add( E element); arrayList.add(int index, E element); arrayList.addAll(Collection<? extends E> c);
讓我們一個個來看看
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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;
}
Integer. MAX_VALUE = 0x7fffffff;
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
第2行:調用ensureCapacityInternal()函數
第8-9行:判斷當前是否是使用默認的構造函數初始化,如果是設置最小的容量為默認容量10,即默認的elementData的大小為10(這里是有一個容器的概念,當前容器的大小一般是大於當前ArrayList的元素個數大小的)
第16行:modCount字段是用來記錄修改過擴容的次數,調用ensureExplicitCapacity()方法意味着確定修改容器的大小,即確認擴容
第26-30、35-44行:一般默認是擴容1.5倍,當時當發現還是不能滿足的話,則使用size+1之后的元素個數,如果發現擴容之后的值大於我們規定的最大值,則判斷size+1的值是否大於MAX_ARRAY_SIZE的值,大於則取值MAX_VALUE,反之則MAX_ARRAY_SIZE,也就數說容器最大的數量為MAX_VALUE
第32行:就是拷貝之前的數據,擴大數組,且構建出一個新的數組
第3行:這時候數組擴容完畢,就是要將需要添加的元素加入到數組中了
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++;
}
第2-3行:判斷插入的下標是否越界
第5行:和上面的一樣,判斷是否擴容
第6行:System.arraycopy這個方法的api在上面已經講過了,這里的話則是將數組elementData從index開始的數據向后移動一位
第8-9行:則是賦值index位置的數據,數組大小加一
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
第2行:將集合轉成數組,這時候源碼沒有對c空很奇怪,如果傳入的Collection為空就直接空指針了
第3-7行:獲取數組a的長度,進行擴容判斷,再將新傳入的數組復制到elementData數組中去
所以對增加數據的話主要調用add、addAll方法,判斷是否下標越界,是否需要擴容,擴容的原理是每次擴容1.5倍,如果不夠的話就是用size+1為容器值,容器擴充后modCount的值對應修改一次
2.3 刪除元素方法
常用刪除方法有以下三種,我們一個個來看看
arrayList.remove(Object o); arrayList.remove(int index) arrayList.removeAll(Collection<?> c)
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
}
從上面源碼可以看出,如果要移除的元素為null和不為空,都是通過for循環找到要被移除元素的第一個下標,所以這里我們就會思考,當我們的集合中有多個null的話,是不是調用remove(null)這個方法只會移除第一個出現的null元素呢?這個需要同學們下去驗證一下。然后通過System.arraycopy函數,來重新組合elementData中的值,且elementData[size]置空原尾部數據 不再強引用, 可以GC掉。
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;
}
可以看到remove(int index)更簡單了,都不需要通過for循環將要刪除的元素下邊確認下來,整體的邏輯和上面通過元素刪除的沒什么區別,再來看看批量刪除
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
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) {
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;
}
第2、6-10行:對傳入集合c進行判空處理
第13-15行:定義局部變量elementData、r、w、modified elementData用來重新指向成員變量elementData,用來存儲最終過濾后的元素,w用來紀錄過濾之后集合中元素的個數,modified用來返回這次是否有修改集合中的元素
第17-19行:for循環遍歷原有的elementData數組,發現如果不是要移除的元素,則重新存儲在elementData,且w自增
第23-28行:如果出現異常,則會導致 r !=size , 則將出現異常處后面的數據全部復制覆蓋到數組里。
第29-36行:判斷如果w!=size,則表明原先elementData數組中有元素被移除了,然后將數組尾端size-w個元素置空,等待gc回收。再修改modCount的值,在修改當前數組大小size的值
2.3 修改元素方法
arrayList.set(int index, E element)
常見的方法也就是上面這一種,我們來看看它的實現的源碼
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下表的元素重新賦值element新值,將老值oldValue返回回去
2.4 查詢元素方法
arrayList.get(int index);
讓我們看看源碼
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
源碼也炒雞簡單,首先去判斷是否越界,如果沒有越界則將index下的元素從elementData數組中取出返回
2.5 清空元素方法
arrayList.clear();
常見清空也就這一個方法
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
源碼也很簡單,for循環重置每一個elementData數組為空,修改size的值,修改modCount值
2.6 判斷是否存在某個元素
arrayList.contains(Object o); arrayList.lastIndexOf(Object o);
常見的一般是contains方法,不過我這里像把lastIndexOf方法一起講了,源碼都差不多
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;
}
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;
}
通過上面的源碼,大家可以看到,不管是contains方法還是lastIndexOf方法,其實就是進行for循環,如果找到該元素則記錄下當前元素下標,如果沒找到則返回-1,很簡單
2.7 遍歷ArrayList中的對象(迭代器)
Iterator<String> it = arrayList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
我們遍歷集合中的元素方法挺多的,這里我們就不講for循環遍歷,我們來看看專屬於集合的iterator遍歷方法吧
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
// Android-changed: Add "limit" field to detect end of iteration.
// The "limit" of this iterator. This is the size of the list at the time the
// iterator was created. Adding & removing elements will invalidate the iteration
// anyway (and cause next() to throw) so saving this value will guarantee that the
// value of hasNext() remains stable and won't flap between true and false when elements
// are added and removed from the list.
protected int limit = ArrayList.this.size;
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor < limit;
}
@SuppressWarnings("unchecked")
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i >= limit)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
第1-3行:在獲取集合的迭代器的時候,去new了一個Itr對象,而Itr實現了Iterator接口,我們主要重點關注Iterator接口的hasNext、next方法
第12-16行:定義變量,limit:用來記錄當前集合的大小值;cursor:游標,默認為0,用來記錄下一個元素的下標;lastRet:上一次返回元素的下標
第18-20行:判斷當前游標cursor的值是否超過當前集合大小zise,如果沒有則說明后面還有元素
第24-31行:在這里面做了不少線程安全的判斷,在這里如果我們異步的操作了集合就會觸發這些異常,然后獲取到集合中存儲元素的elemenData數組
第32-33行:游標cursor+1,然后返回元素 ,並設置這次次返回的元素的下標賦值給lastRet
3,看源碼之前問題的反思
ok,上面的話基本上把我們ArrayList常用的方法的源碼給看完了。這時候,我們需要來對之前的問題來一一進行總結了
①有序、可重復是什么概念?
public static void main(String[] args){
ArrayList arrayList = new ArrayList();
arrayList.add("1");
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.add("1");
Iterator<String> it = arrayList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
輸出結果
1
1
2
3
1
可重復是指加入的元素可以重復,有序是指的加入元素的順序和取出來的時候順序相同,一般這個特點是List相對於Set和Map來比較出來的,后面我們把Set、Map的源碼看了之后會更加理解這兩個特點
② 為什么說查找查找元素比較快,但添加和刪除元素比較慢呢?
我們從上面的源碼得到,當增加元素的時候是有可能會觸發擴容機制的,而擴容機制會導致數組復制;刪除和批量刪除會導致找出兩個集合的交集,以及數組復制操作;而查詢直接調用return (E) elementData[index]; 所以說增、刪都相對低效 而查找是很高效的操作。
③ 為什么說ArrayList線程是不安全
從上面的代碼我們都知道,現在add()方法為例
public boolean add(E e) {
//確定是否擴容,這里可以忽略
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
這里我們主要看兩點,第一點add()方法前面沒有synchronized字段、第二點 elementData[size++] = e;這段代碼可以拆開為下面兩部分代碼
elementData[size] = e; size++
也就是說整個add()方法可以拆為兩步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。我們都知道我們的CUP是切換進程運行的,在單線程中這樣是沒有問題的,但是一般在我們項目中很多情況是在多線程中使用ArrayList的,這時候比如有兩個線程,線程 A 先將元素存放在位置 0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也向此 ArrayList 添加元素,因為此時 Size 仍然等於 0 ,所以線程B也將元素存放在位置0。然后線程A和線程B都繼續運行,都增加 Size 的值。這樣就會得到元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這樣就造成了我們的線程不安全了。
大家可以寫一個線程搞兩個線程來試試,看看size是不是有問題,這里就不帶大家一起寫了。
④ transient 關鍵字有什么用?
唉,這個就有點意思了,這個是我們之前讀源碼讀出來的遺留問題,那源碼現在讀完了,是時候來解決這個問題了,我們來看看transient官方給的解釋是什么
當對象被序列化時(寫入字節序列到目標文件)時,transient阻止實例中那些用此關鍵字聲明的變量持久化;當對象被反序列化時(從源文件讀取字節序列進行重構),這樣的實例變量值不會被持久化和恢復。
然后我們看一下ArrayList的源碼中是實現了java.io.Serializable序列化了的,也就是transient Object[] elementData; 這行代碼的意思是不希望elementData被序列化,那這時候我們就有一個疑問了,為什么elementData不進行序列化?這時候我去網上找了一下答案,覺得這個解釋是最合理且易懂的
在ArrayList中的elementData這個數組的長度是變長的,java在擴容的時候,有一個擴容因子,也就是說這個數組的長度是大於等於ArrayList的長度的,我們不希望在序列化的時候將其中的空元素也序列化到磁盤中去,所以需要手動的序列化數組對象,所以使用了transient來禁止自動序列化這個數組
這時候我們是懂了為什么不給elementData進行序列化了,那當我們要使用序列化對象的時候,elementData里面的數據是不是不能使用了?這里ArrayList的源碼提供了下面方法
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();
}
}
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
通過writeObject方法將數據非null數據寫入到對象流中,再使用readObject讀取數據
4,總結
上面我們寫了這么一大篇,是時候該來總結總結一下了
①查詢高效、但增刪低效,增加元素如果導致擴容,則會修改modCount,刪出元素一定會修改。 改和查一定不會修改modCount。增加和刪除操作會導致元素復制,因此,增刪都相對低效。而在我們常見的Android場景中,ArrayList多用於存儲列表的數據,列表滑動時需要展示每一個Item(element)的數組,所以查詢操作是最高頻的,且增加操作只有在列表加載更多時才會用到 ,而且是在列表尾部插入,所以也不需要移動數據的操作。而刪操作則更低頻。 故選用ArrayList作為保存數據的結構
②線程不安全,這個特點一般會和Vector做比較,Vector的源碼,內部也是數組做的,區別在於Vector在API上都加了synchronized所以它是線程安全的,以及Vector擴容時,是翻倍size,而ArrayList是擴容50%。Vector的源碼大家可以在后面閑下來的時候看看,這里給大家留一個思考題:既然Vector是安全的,那為什么我們在日常開發Android中基本上沒有用到Vector呢?大家可以閑下來的時候來尋找一下這個問題的答案
最后再啰嗦一句,寫完全篇后發現 ,感覺好久沒寫博客手很生了,在寫的過程總發現大體框架不對也在一點點的修復,后面爭取堅持寫下來,加油!!!
