本文基於JDK1.8版本,對集合中的巨頭ArrayList作必定的源碼學習,將會參考大量資料,在文章後面都將會給出參考文章連接,本文用以鞏固學習知識。c++
ArrayList繼承了AbstracList這個抽象類,還實現了List接口,提供了添加、刪除、修改、遍歷等功能。至於其餘接口,之後再作總結。git
底層基於數組實現,咱們能夠查看源碼,瞭解其擁有的一些屬性:github
private static final long serialVersionUID = 8683452581122892189L; //默認的初始容量爲10 private static final int DEFAULT_CAPACITY = 10; //若是指定數組容量爲0,返回該數組,至關於new ArrayList<>(0); private static final Object[] EMPTY_ELEMENTDATA = {}; //沒有指定容量時,返回該數組,與上面不一樣的是:new ArrayList<>(); private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //該數組保存着ArrayList存儲的元素,任何沒有指定容量的ArrayList在添加第一個元素後,將會擴容至初始容量10 transient Object[] elementData; // non-private to simplify nested class access //表明了當前存儲元素的數量 private int size;
再次強調將EMPTY_ELEMENTDATA
和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
區分開來是爲了明確添加第一個元素時,應該擴容的大小,具體擴容的機制,後面會分析。數組
咱們再來瞧瞧它的構造器:安全
//該構造器用以建立一個能夠指定容量的列表 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { //建立一個指定容量大小的數組 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //指定容量爲0,對應EMPTY_ELEMENTDATA數組 this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } //默認無參構造器,賦值空數組,可是在第一次添加以後,容量變爲默認容量10 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //傳入一個集合,根據該集合迭代器返回順序,構造一個指定集合裏元素的列表 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); //傳入集合不爲空長 if ((size = elementData.length) != 0) { //傳入集合轉化爲的數組可能不是Object[]須要判斷 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { //傳入集合爲元素數量爲0,用空數組代替便可 this.elementData = EMPTY_ELEMENTDATA; } } //指定集合爲null的話(並非說集合爲空長),調用ArrayList的toArray方法,可能會拋出空指針異常
瞭解完ArrayList基本的屬性和構造器以後,咱們將對裏面包含的方法進行學習:ide
//沒有指定索引,默認在尾部添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! //擴容以後,下一位賦值爲e,size加1 elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } //判斷是否爲默認構造器生成的數組,並將minCapacity置爲0;若是不是,minCapacity仍是傳入的size+1 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //使用默認構造器,那麼纔會返回所須要的最小容量爲默認容量10 return Math.max(DEFAULT_CAPACITY, minCapacity); } //minCapacity = size+1 return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { //定義在AbstractList中,用於存儲結構修改次數 modCount++; //若是最小容量比數組總長度還大,就擴容 if (minCapacity - elementData.length > 0) grow(minCapacity); } //擴容操做 private void grow(int minCapacity) { int oldCapacity = elementData.length; //將舊容量右移一位在加上自己,像當於新容量爲就容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //1.新數組的容量仍是不能知足須要的最小容量,如初始指定容量爲0時的狀況 //2.新數組越過了整數邊界,newCapacity將會小於0 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //若是新數組的容量比數組最大的容量Integer.MAX_VALUE - 8還大, //調用hugeCapacity方法 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } //比較最小容量和MAX_ARRAY_SIZE private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //三目表達式:若是真的須要擴這麼大容量的狀況下: //1.最小容量大於MAX_ARRAY_SIZE,新容量等於Integer.MAX_VALUE,不然新容量爲Integer.MAX_VALUE-8 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
public void ensureCapacity(int minCapacity)
方法預先設置容量。參考:https://www.iteye.com/topic/577602。(可是通過個人我的測試:在1億級或以上地數量上,沒有調用該方法要快一些,可是真實場景應該不會把這麼多的數據存放在裏面吧,因此能夠的話,用上這個方法,提高性能呀。)newCapacity - minCapacity < 0
的思考,很容易能看出斷定條件是新容量<須要的最小容量。可是這個條件怎樣才能達到呢
注意:移位運算效率會比整除運算更高一些。性能
其實擴容的方式就是咱們看到的,建立一個以新容量爲長度的新數組,並將原來數組的值所有拷貝到新數組上,最後讓elementData指向這個新數組。學習
文章寫到這裏,我大舒一口長氣,層層嵌套的調用終於結束了,不知道大家的心裏是否也和我同樣哈。咱們趁熱打鐵,趕忙看看另外一個重載的add方法。測試
//在索引爲index處插入E public void add(int index, E element) { //索引越界判斷 rangeCheckForAdd(index); //同上,確保有足夠容量添加元素 ensureCapacityInternal(size + 1); // Increments modCount!! //實際上Arrays.copyOf的底層調用的就是這個方法,意思是在原數組上從索引的位置到最後總體向後複製一位,至關於移動的長度爲 (size-1) - index +1 = size -index System.arraycopy(elementData, index, elementData, index + 1, size - index); //在將index處填上元素E elementData[index] = element; //元素數量+1 size++; }
有了前面的鋪墊,相對來講就比較輕鬆了。咱們不妨看看判斷數組越界的方法,媽呀,這就更加清晰了,可是須要注意的是index==size在添加操做裏,至關於從尾部插入,並不會構成越界:
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
看完了「增」的兩個方法,該輪到同是四胞胎兄弟的「刪」了。再談「刪」以前,咱們要明確,ArrayList底層基於數組實現,可依靠連續索引值存取獲取數據就變得理所固然了:
E elementData(int index) { return (E) elementData[index]; }
下面是「刪」操做,須要注意的是,remove操做並不可以將容量減小,只是將其中的元素數量變少,自始至終只是size在變化,不信你看:
//移除指定位置的元素,並將其返回 public E remove(int index) { //範圍判斷 rangeCheck(index); //操做列表,計數加1 modCount++; //取出舊值 E oldValue = elementData(index); //至關於把index+1位置向後的全部元素集體向前複製一位,複製的長度就是 //(size-1)-(index+1)+1 = numMoved int numMoved = size - index - 1; if (numMoved > 0) //執行集體拷貝動做 System.arraycopy(elementData, index+1, elementData, index, numMoved); //並讓最後一個空出來的位置指向null,點名讓GC清理 elementData[--size] = null; //返回舊值 return oldValue; }
能夠稍微看一下rangeCheck的代碼,與add操做裏斷定略有不一樣,省去了index<0的判斷,我一開始很疑惑,後來發現後面有對數組的索引值取值,仍是會發生異常:
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
我以爲有必要總結一下System.arraycopy這個方法,
public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
native修飾符,底層並非Java實現,而是c和c++。
這個方法的做用呢:就是從指定的源數組(src)從指定位置(srcPos)開始複製數組到目標數組(dest)的指定位置(destPos),複製的個數正好是length。
而Arrays.copyOf這個方法雖然底層調用了System.arraycopy,可是使用上是不太同樣的,它不須要目標數組,系統會自動在內部新建一個數組,並返回。
哇,感受add部分講完,真的思路及其清晰,簡直豁然開朗呢。我們繼續來remove!
//移除指定元素,找到並刪除返回true,沒找到返回false public boolean remove(Object o) { //判斷指定的元素是否自己就是null值 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++) //不是空值的話,就找值相等的,注意不要elementData[index].equals(o),時刻避免空指針 if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
還有一個範圍性的removeRange就不贅述了,總結一下:ArrayList中的remove操做基於數組的拷貝,並將remove的長度置空,元素數量相應減小(只是元素數量減小,數組容量並不會改變)。
對了,清理的話,clear方法會清理的相對乾淨一些,可是依舊只是size變化:
public void clear() { modCount++; //將全部元素置空,等待GC寵幸 for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
固然,若是你但願數組容量也發生變化的話。你能夠試試下面的這個方法:
//將ArrayList容量調整爲當前size的大小 public void trimToSize() { modCount++; //基於三目運算 if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
接下來,講一講至關簡單的set與get這對基佬操做:
//用指定值替換隻當索引位置上的值 public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } //獲取指定索引位置上的值 public E get(int index) { rangeCheck(index); return elementData(index); }
而後是姐妹花操做:indexOf和lastIndexOf。(ps:尋找元素的過程能夠參考remove指定元素的過程),以indexOf爲例,lastIndexOf從尾部向前遍歷便可。
//判斷o在ArrayList中第一次出現的位置 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; }
經過indexOf方法的返回值,咱們還能夠判斷某個元素是否存在:
public boolean contains(Object o) { return indexOf(o) >= 0; }
除了單個元素增以外,ArrayList中還提供了能夠將整個集合增長到自己尾部的方法:
//把傳入集合中的全部元素所有加到自己集合的後面,若是發生改變就返回true public boolean addAll(Collection<? extends E> c) { //將傳入集合轉化爲列表,若是傳入集合爲null,會發生空指針異常 Object[] a = c.toArray(); int numNew = a.length; //肯定新長度是否須要擴容 ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; //傳入爲空集合就爲false,由於不會發生改變 return numNew != 0; }
它的重載方法是在指定位置插入另外一個集合中地全部元素,而且以迭代的順序排列:
//在指定位置插入另外一集合中的全部元素 public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); //仍是會引起空指針 Object[] a = c.toArray(); //傳入新集合c的元素個數 int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount //要移動的個數:(size-1)-index+1 = numMoved int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //size<index的狀況,前面就會拋異常,因此這裏只能index==size,至關於從尾部添加 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
ArrayList基於數組實現,查詢便利,經過擴容機制實現動態增加。
對了,它是線程不安全的,這個之後學習的時候在作總結吧。
對了若是不出意外的話,以後會帶來LinkedList的源碼學習,若是以爲我有敘述錯誤的地方,或者我沒有說明白點地方,還望評論區批評指正,一塊兒學習交流,加油加油!
參考連接:
淺談ArrayList動態擴容
List集合就這麼簡單【源碼剖析】
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md