本文基於JDK-8u261源碼分析java
ArrayList做爲最基礎的集合類,其底層是使用一個動態數組來實現的,這裏「動態」的意思是能夠動態擴容(雖然ArrayList能夠動態擴容,但卻不會動態縮容)。可是與HashMap不一樣的是,ArrayList使用的是1.5的擴容策略,而HashMap使用的是2的方式。還有一點與HashMap不一樣:ArrayList的默認初始容量爲10,而HashMap爲16。面試
有意思的一點是:在Java 7以前的版本中,ArrayList的無參構造器是在構造器階段完成的初始化;而從Java 7開始,改成了在add方法中完成初始化,也就是所謂的延遲初始化。在HashMap中也有一樣的設計思路。算法
另外,同HashMap同樣,若是要存入一個很大的數據量而且事先知道要存入的這個數據量的固定值時,就能夠往構造器裏傳入這個初始容量,以此來避免之後的頻繁擴容。數組
/** * ArrayList: * 無參構造器 */ public ArrayList() { //DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現「{}」,這裏也就是在作初始化 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 有參構造器 */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { //initialCapacity>0就按照這個容量來初始化數組 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //EMPTY_ELEMENTDATA也是一個空實現「{}」,這裏也是在作初始化 this.elementData = EMPTY_ELEMENTDATA; } else { //若是initialCapacity爲負數,則拋出異常 throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
添加指定的元素:數據結構
/** * ArrayList: */ public boolean add(E e) { //查看是否須要擴容 ensureCapacityInternal(size + 1); //size記錄的是當前元素的個數,這裏就直接往數組最後添加新的元素就好了,以後size再+1 elementData[size++] = e; return true; } /** * 第6行代碼處: */ private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { /* minCapacity = size + 1 以前說過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現「{}」,這裏也就是在判斷是否是調用的無參構造器 並第一次調用到此處 */ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { /* 若是是的話就返回DEFAULT_CAPACITY(10)和size+1之間的較大者。也就是說,數組的最小容量是10 這裏有意思的一點是:調用new ArrayList<>()和new ArrayList<>(0)兩個構造器會有不一樣的默認容量(在HashMap中 也是如此)。也就是說無參構造器的初始容量爲10,而傳進容量爲0的初始容量爲1。同時這也就是爲何會有 EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA這兩個常量的存在,雖然它們的值都是「{}」 緣由就在於無參構造器和有參構造器徹底就是兩種不一樣的實現策略:若是你想要具體的初始容量,那麼就調用有參構造器吧, 即便傳入的是0也是符合這種狀況的;而若是你不在意初始的容量是多少,那麼就調用無參構造器就好了,這會給你默 認爲10的初始容量 */ return Math.max(DEFAULT_CAPACITY, minCapacity); } //若是調用的是有參構造器,或者調用無參構造器但不是第一次進來,就直接返回size+1 return minCapacity; } /** * 第16行代碼處: */ private void ensureExplicitCapacity(int minCapacity) { //修改次數+1(快速失敗機制) modCount++; /* 若是+1後指望的容量比實際數組的容量還大,就須要擴容了(若是minCapacity也就是size+1後發生了數據溢出, 那麼minCapacity就變爲了一個負數,而且是一個接近int最小值的數。而此時的elementData.length也會是一個接近 int最大值的數,那麼該if條件也有可能知足,此時會進入到grow方法中的hugeCapacity方法中拋出溢出錯誤) */ if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { //獲取擴容前的舊數組容量 int oldCapacity = elementData.length; //這裏擴容後新數組的容量是採用舊數組容量*1.5的方式來實現的 int newCapacity = oldCapacity + (oldCapacity >> 1); /* 若是新數組容量比+1後指望的容量還要小,此時把新數組容量修正爲+1後指望的容量(對應於newCapacity爲0或1的狀況) 這裏以及後面的判斷使用的都是「if (a - b < 0)」形式,而不是常規的「if (a < b)」形式是有緣由的, 緣由就在於須要考慮數據溢出的狀況:若是執行了*1.5的擴容策略後newCapacity發生了數據溢出,那麼它就同樣 變爲了一個負數,而且是一個接近int最小值的數。而minCapacity此時也一定會是一個接近int最大值的數, 那麼此時的「newCapacity - minCapacity」計算出來的結果就可能會是一個大於0的數。因而這個if條件 就不會執行,而是會在下個條件中的hugeCapacity方法中處理這種溢出的問題。這同上面的分析是相似的 而若是這裏用的是「if (newCapacity < minCapacity)」,數據溢出的時候該if條件會返回true,因而 newCapacity會錯誤地賦值爲minCapacity,而沒有使用*1.5的擴容策略 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; /* 若是擴容後的新數組容量比設定好的容量最大值(Integer.MAX_VALUE - 8)還要大,就從新設置一下新數組容量的上限 同上面的分析,若是發生數據溢出的話,這裏的if條件也多是知足的,那麼也會走進hugeCapacity方法中去處理 */ if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); /* 能夠看到這裏是經過Arrays.copyOf(System.arraycopy)的方式來進行數組的拷貝, 容量是擴容後的新容量newCapacity,將拷貝後的新數組賦值給elementData便可 */ elementData = Arrays.copyOf(elementData, newCapacity); } /** * 第83行代碼處: */ private static int hugeCapacity(int minCapacity) { //minCapacity對應於size+1,因此若是minCapacity<0就說明發生了數據溢出,就拋出錯誤 if (minCapacity < 0) throw new OutOfMemoryError(); /* 若是minCapacity大於MAX_ARRAY_SIZE,就返回int的最大值,不然返回MAX_ARRAY_SIZE 無論返回哪一個,這都會將newCapacity從新修正爲一個大於0的數,也就是處理了數據溢出的狀況 其實從這裏能夠看出:本方法中並無使用*1.5的擴容策略,而只是設置了一個上限而已。可是在Java中 真能申請獲得Integer.MAX_VALUE這麼大的數組空間嗎?其實不見得,這只是一個理論值。實際上須要考慮 -Xms和-Xmx等一系列JVM參數所設置的值。因此這也可能就是MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) 其中-8的含義吧。但無論如何,當數組容量達到這麼大的量級時,乘不乘1.5其實已經不過重要了) */ return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
在指定的位置處添加指定的元素:併發
/** * ArrayList: */ public void add(int index, E element) { //index參數校驗 rangeCheckForAdd(index); //查看是否須要擴容 ensureCapacityInternal(size + 1); /* 這裏數組拷貝的意義,就是將index位置處以及後面的數組元素日後移動一位,以此來挪出一個位置 System.arraycopy是直接對內存進行復制,在大數據量下,比for循環更快 */ System.arraycopy(elementData, index, elementData, index + 1, size - index); //而後將須要插入的元素插入到上面挪出的index位置處就能夠了 elementData[index] = element; //最後size+1,表明添加了一個元素 size++; } /** * 第6行代碼處: * 檢查傳入的index索引位是否越界,若是越界就拋異常 */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private String outOfBoundsMsg(int index) { return "Index: " + index + ", Size: " + size; }
/** * ArrayList: */ public E get(int index) { //index參數校驗 rangeCheck(index); //獲取數據 return elementData(index); } /** * 第6行代碼處: * 這裏只檢查了index大於等於size的狀況,而index爲負數的狀況 * 在elementData方法中會直接拋出ArrayIndexOutOfBoundsException */ private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * 第8行代碼處: * 能夠看到,這裏是直接從elementData數組中獲取指定index位置的數據 */ @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; }
刪除指定的元素:工具
/** * ArrayList: */ public boolean remove(Object o) { if (o == null) { //若是要刪除的元素爲null for (int index = 0; index < size; index++) //遍歷數組中的每個元素,找到第一個爲null的元素 if (elementData[index] == null) { /* 刪除這個元素,並返回true。這裏也就是在作清理的工做:遇到一個爲null的元素就清除掉 注意這裏只會清除一次,並不會所有清除 */ fastRemove(index); return true; } } else { //若是要刪除的元素不爲null for (int index = 0; index < size; index++) //找到和要刪除的元素是一致的數組元素 if (o.equals(elementData[index])) { /* 找到了一個就進行刪除,並返回true。注意這裏只會找到並刪除一個元素, 若是要刪除全部的元素就調用removeAll方法便可 */ fastRemove(index); return true; } } /* 若是要刪除的元素爲null而且找不到爲null的元素,或者要刪除的元素不爲null而且找不到和要刪除元素相等的數組元素, 就說明此時不須要刪除元素,直接返回false就好了 */ return false; } /** * 第14行和第26行代碼處: */ private void fastRemove(int index) { //修改次數+1 modCount++; //numMoved記錄的是移動元素的個數 int numMoved = size - index - 1; if (numMoved > 0) /* 這裏數組拷貝的意義,就是將index+1位置處以及後面的數組元素往前移動一位, 這會將index位置處的元素被覆蓋,也就是作了刪除 */ System.arraycopy(elementData, index + 1, elementData, index, numMoved); /* 由於上面是左移了一位,因此最後一個位置至關於騰空了,這裏也就是將最後一個位置(--size)置爲null 固然若是上面計算出來的numMoved自己就小於等於0,也就是index大於等於size-1的時候(大於不太可能, 是屬於異常的狀況),意味着不須要進行左移。此時也將最後一個位置置爲null就好了。置爲null以後, 原有數據的引用就會被斷開,GC就能夠工做了 */ elementData[--size] = null; }
刪除指定位置處的元素:源碼分析
/** * ArrayList: */ public E remove(int index) { //index參數校驗 rangeCheck(index); //修改次數+1 modCount++; //獲取指定index位置處的元素 E oldValue = elementData(index); //numMoved記錄的是移動元素的個數 int numMoved = size - index - 1; if (numMoved > 0) /* 同上面fastRemove方法中的解釋,這裏一樣是將index+1位置處以及後面的數組元素往前移動一位, 這會將index位置處的元素被覆蓋,也就是作了刪除(這裏是否能夠考慮封裝?) */ System.arraycopy(elementData, index + 1, elementData, index, numMoved); //同上,將最後一個位置(--size)置爲null elementData[--size] = null; //刪除以後,將舊值返回就好了 return oldValue; }
這是《阿里巴巴編碼規範》中的一條。正例:大數據
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("2".equals(item)) { iterator.remove(); } }
反例:this
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String item : list) { if ("2".equals(item)) { list.remove(item); } }
運行上面的代碼能夠看到,使用迭代器的刪除操做是不會有問題、能成功刪除的;而使用foreach循環進行刪除則會拋出ConcurrentModificationException異常,但若是使用foreach循環刪除第一個元素「1」的時候又會發現不會拋出異常。那麼這究竟是爲何呢?
衆所周知,foreach循環是一種語法糖,那麼其背後究竟是如何來實現的呢?將上面反例的代碼反編譯後的結果以下:
public class com.hys.util.Test { public com.hys.util.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #4 // String 1 11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: aload_1 18: ldc #6 // String 2 20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 25: pop 26: aload_1 27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 32: astore_2 33: aload_2 34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 39: ifeq 72 42: aload_2 43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 48: checkcast #10 // class java/lang/String 51: astore_3 52: ldc #6 // String 2 54: aload_3 55: invokevirtual #11 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 58: ifeq 69 61: aload_1 62: aload_3 63: invokeinterface #12, 2 // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z 68: pop 69: goto 33 72: return }
上面的內容不須要徹底看懂,只須要看到第23行代碼處、第26行代碼處和第29行代碼處後面的解釋,也就是foreach循環是經過調用List.iterator方法來生成一個迭代器,經過Iterator.hasNext方法和Iterator.next方法來實現的遍歷操做(普通的for循環不是經過這種方式,也就是說普通的for循環不會有這種問題)。
那麼首先來看一下ArrayList中iterator方法的實現:
public Iterator<E> iterator() { return new Itr(); }
能夠看到是返回了一個內部類Itr:
/** * An optimized version of AbstractList.Itr */ 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; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } //... }
而拋出異常是在上面第17行代碼處的checkForComodification方法裏面拋出的,下面來看一下它的實現:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
能夠看到若是modCount和expectedModCount不等就會拋出ConcurrentModificationException異常。而上面說過,在add方法和remove方法中,會對modCount修改標誌位作+1的操做。這裏的modCount是爲了作快速失敗用的。快速失敗指的是若是在遇到併發修改時,迭代器會快速地拋出異常,而不是在未來某個不肯定的時間點冒着任意而又不肯定行爲的風險來進行操做,也就是將可能出現的bug點推前。在包括HashMap在內的絕大部分集合類都是有快速失敗機制的。注意:這裏的併發修改指的並不都是發生在併發時的修改,也有多是在單線程中所作的修改致使的,就如同上面的反例同樣。
這裏拿上面的反例來舉例,ArrayList調用了兩次add方法,也就是此時的modCount應該爲2。而expectedModCount如上所示,一開始會初始化爲modCount的值,也就是也爲2。
首先來看一下刪除「2」的狀況:
第一次循環:
由於此時的modCount和expectedModCount都爲2,因此第一次循環中不會拋出異常,拋出異常都是發生在不是第一次循環的狀況中。在next方法走完後,foreach循環方法體中的remove方法的if條件判斷不知足,就結束了本次循環。
第二次循環:
第二次循環的hasNext和next方法都是能成功走完的,在這以後會進入到foreach循環方法體中的remove方法中,進行刪除元素。而此時的size-1變爲了1。上面分析過,在remove方法中的fastRemove方法中,會對modCount+1,也就變爲了3。
第三次循環:
而後會走入到第三次循環中的hasNext方法中。按照正常的狀況下該方法是會返回false的,但由於此時的size已經變爲了1,而此時的cursor爲2(cursor表明下一次的索引位置),因此二者不等,錯誤地返回了true,因此會繼續走入到next方法中的checkForComodification方法中,判斷此時的modCount和expectedModCount是否相等。由於此時的modCount已經變爲了3,和expectedModCount的值爲2不等,因此在此拋出了ConcurrentModificationException異常。
再來看一下刪除「1」的時候爲何不會拋出異常:
第一次循環:
同上,此時的modCount和expectedModCount都爲2,因此第一次循環中的hasNext和next方法都不會拋異常。在這以後會進入到foreach循環方法體中的remove方法中,進行刪除元素。同上,size-1變爲了1,而modCount+1變爲了3。
第二次循環:
在第二次循環的hasNext方法中,此時的cursor爲1,而size也是1,二者相等。因此hasNext方法返回false,就跳出了foreach循環,不會走到隨後的next方法中,也就不會拋出異常。
而後來看一下add操做的狀況,其實在第一次循環中添加元素和不是第一次循環中添加元素、從而拋出異常的緣由是相似的,這裏就以第一次循環中添加元素來舉例:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.add(item); } }
第一次循環:
同上,此時的modCount和expectedModCount都爲2,因此第一次循環中的hasNext和next方法都不會拋異常。在這以後會進入到foreach循環方法體中的add方法中,進行添加元素。size+1變爲了3,而modCount+1也變爲了3。
第二次循環:
在第二次循環的hasNext方法中,此時的cursor爲1,而size爲3,二者不等,因此hasNext方法返回true,會走到隨後的next方法中。而在next方法中的checkForComodification方法中,此時的modCount已經變爲了3,而expectedModCount仍是爲2。二者不等,因此在此拋出了ConcurrentModificationException異常。
其實從上面的幾回分析中就能夠看出:只要在foreach循環方法體中有進行修改過modCount和size的操做,就都有可能會是拋出異常的。
既然如今已經知道了foreach循環中使用remove/add操做拋出異常的緣由,那麼就能夠分析一下爲何使用迭代器進行相關操做就不會有問題呢?上面正例代碼中的第5行代碼處的iterator方法、第6行和第7行代碼處的hasNext和next方法都是跟foreach循環裏的實現是同樣的,而區別在於第9行代碼處的remove操做。這裏的remove不是ArrayList中的remove操做,而是Itr內部類中的remove操做:
public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
能夠看到第7行代碼處是調用了ArrayList的remove操做進行刪除的,但同時注意第10行代碼處會將expectedModCount更新爲此時modCount的最新值,這樣在next方法中就不會拋出異常了;在第8行代碼處會將cursor更新爲lastRet(lastRet表明上一次的索引位置),即將cursor-1(由於此時要remove,因此cursor指針須要減一)。這樣在hasNext方法中就會返回正確的值了。
雖然iterator方法能夠提供remove操做來使刪除能正確執行,但其卻沒有提供相關add方法的API。無妨, ArrayList中爲咱們提供了listIterator方法,其中就有add操做(若是必定要用迭代器方式來實現的話)。相關的示例代碼以下:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); ListIterator<String> iterator = list.listIterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("1".equals(item)) { iterator.add(item); } }
同上,首先來看一下第5行代碼處的listIterator方法:
public ListIterator<E> listIterator() { return new ListItr(0); }
listIterator方法返回了一個ListItr內部類。那麼就來看一下ListItr的代碼實現:
private class ListItr extends Itr implements ListIterator<E> { ListItr(int index) { super(); cursor = index; } //... public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } }
能夠看到ListItr內部類是繼承了Itr類,包括hasNext和next等方法都是直接複用的。而在add方法中的第14行代碼處,是調用了ArrayList的add操做進行添加的。另外和Itr的remove方法同樣,第17行代碼處也是在更新expectedModCount爲此時modCount的最新值,第15行代碼處的cursor更新爲+1後的結果(由於此時是在作add操做)。這樣後續的hasNext和next操做就不會有問題了。
在應用業務裏待過久不少底層的東西每每容易忽略掉,今年的年初計劃是把經常使用的JDK源碼工具作一次總結,眼看年末將近,乘着最近有空,趕忙的給補上。在這行乾的越久真是越以爲:萬丈高樓平地起,這絕B是句真理!
每一次總結都是對知識點掌握程度的審視,技術不易,每日精進一點,與你們共勉。