1,前言java
好久沒有寫博客了,很想念你們,18年都快過完了,纔開始寫第一篇,爭取後面每週寫點,權當是記錄,由於最近在看JDK的Collection,並且ArrayList源碼這一塊也常常被面試官問道,因此今天也就和你們一塊兒來總結一下面試
2,源碼解讀api
當咱們通常提到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 ui
第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; }
第二、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呢?你們能夠閒下來的時候來尋找一下這個問題的答案
最後再囉嗦一句,寫徹底篇後發現 ,感受很久沒寫博客手很生了,在寫的過程總髮現大致框架不對也在一點點的修復,後面爭取堅持寫下來,加油!!!