Java中Arraylist源碼分析
前言:ArrayList作為我們常用的一個集合數據類型,我們在代碼中經常用它來裝載數據,可謂是和HashMap一樣常用的集合類型了。我們經常用它,那麽就有必須知道它的內部工作原理,比如它是如何添加進去數據的,它內部的數據結構是怎樣的,當我們做一個remove操作,它又做了哪些工作。了解這些內部工作的原理能夠幫助我們更好的理解Arraylist,什麽時候使用它和不使用它,如何提升它的效率,等等。那麽本篇博文就來聚焦Arraylist,走進它的內部源碼,來一探究竟吧。
本篇博客的目錄
一:Arraylist簡介
二:Arrsylist的構造方法
三:Arraylist的add和remove方法分析
四:Arraylist的常用方法源碼分析
五:Arrraylist與linkedlist的區別
六:總結
一:Arraylist簡介:
首先是Arraylist一種java的數據容器,作為數據容器,我們在程序中經常使用它,比如Map、Collection、HashMap、TreeSet等等。如它的名字一樣,它的內部結構是數組,而這個數組是可以動態擴容的,就猶如hashMap一樣。它的所有操作都是基於其內部的動態數組來進行操作,list接口的大小可變數組的實現、如果1-1所示(其中index表示的是數組的元素的位置,但註意其數組下標實際是從0開始的,而不是1),源碼中多次涉及到index這個變量,在這裏可以先理解一下。其次它本質上一個類,實現了list接口,繼承了AbstractList,和Cloneable接口、Serializable表明其繼承於抽象的list,並且可以進行淺復制和序列化寫入到數據文件中。同時它在構建的時候就說明了其類型泛型E,所以Arraylist是一種在新建時就聲明其包含類型的集合,並且可支持復制、序列化、克隆等特性。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
二:Arrsylist的成員變量和構造方法
我們先來看一看Arraylist的成員變量,從中可以看到Arraylist維護著的數組和一些基本的數值表示出它的大小和數據類型,之所以聲明為Object類型的,是因為要定義一個適用於所有java數據類型的,這樣它就可以向下轉型為所有基本類型了。至於元素容量為什麽是10,而不是11或者12,這個可能是jdk的作者經過計算、根據使用習慣計算出來的,有一定的科學依據。這些都是全局變量,也就是說下面的方法都可以訪問這些變量,並修改它的值。
private static final long serialVersionUID = 8683452581122892189L; //序列版本號 private static final int DEFAULT_CAPACITY = 10;//默認數組元素的初始容量為10 private static final Object[] EMPTY_ELEMENTDATA = {};//空元素對象數組 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//默認容量的空數據數組 transient Object[] elementData; // 數組元素對象,list中的元素就存在這裏 private int size;//添加的元素的數量
看完了成員變量,我們來看一下Arraylist的3個構造函數,其中我們用到的最多的也就是第二個無參的構造函數,平時代碼裏我們經常這樣寫:List<String> list= new Arraylist()<String>;這就是調用了其第二個構造函數,在構造函數裏面,我們可以看到它對空數組進行了賦值,將維護著的數組的大小設為10個容量,然後我們就可以使用這個list了。接下來我們就講一講如何往這個list裏面增添元素和移除元素,這是我們使用list最常用的兩個方法。
public ArrayList(int initialCapacity) { //初始化容量 if (initialCapacity > 0) { //判斷傳入的參數值 this.elementData = new Object[initialCapacity];//把傳入的容量作為數組的初始容量 } else if (initialCapacity == 0) {//如果傳入的是0 this.elementData = EMPTY_ELEMENTDATA;//初始化為空數組 } else { throw new IllegalArgumentException("Illegal Capacity: "+ //傳入的如果小於0,就拋出參數違規異常 initialCapacity); } } public ArrayList() { //空構造方法 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//空對象數組初始化,其容量為10 } 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; } }
三:Arraylist的add和remove方法分析
1.add方法的解析:
我們先來看一下add方法,這其中又涉及到了rangeCheckForAdd方法和ensureCapacityInternal、System.arraycopy方法,我們來依次分析一下其中的源碼,看看究竟在我們add一個元素的時候,Arraylist做了什麽:
public void add(int index, E element) {//根據指定元素添加指定元素 rangeCheckForAdd(index);//通過位置進行範圍檢查 ensureCapacityInternal(size + 1); // System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element;//元素賦值給指定的位置 size++;//數組大小增加 }
private void rangeCheckForAdd(int index) { //專為add方法增加的返回檢查 if (index < 0 || index > this.size)//如果傳入的參數小於0或者大於數組已添加元素的個數 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));//拋出異常 }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//定義數組最大的值為Integer最大值-8
private void ensureCapacityInternal(int minCapacity) {//確保容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果數組元素等於默認的空數組容量 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//取默認的容量和指定的容量的較大值 } ensureExplicitCapacity(minCapacity);//傳入 ensureExplicitCapacity方法確保擴展的容量 }
private void ensureExplicitCapacity(int minCapacity) {/確保擴展容量 modCount++;//修改次數+1 // overflow-conscious code if (minCapacity - elementData.length > 0)//如果指定的容量大於數組容量 grow(minCapacity);//調用gorw方法 }
private void grow(int minCapacity) { //傳入指定的容量 // overflow-conscious code int oldCapacity = elementData.length;//取舊的數組元素的長度 int newCapacity = oldCapacity + (oldCapacity >> 1);//舊長度除以2+長度賦予新長度(相當於擴容1.5倍) if (newCapacity - minCapacity < 0)//如果新長度小於指定的容量 newCapacity = minCapacity;//把新計算的長度賦予指定的容量 if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新容量>最大數組大小 newCapacity = hugeCapacity(minCapacity);//調用bugeCapcaity方法 elementData = Arrays.copyOf(elementData, newCapacity);//復制數組 }
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 如果指定容量小於0 throw new OutOfMemoryError();//拋出異常內存錯誤 return (minCapacity > MAX_ARRAY_SIZE) ?//如果指定容量大於最大數組大小返回int的最大值 Integer.MAX_VALUE : MAX_ARRAY_SIZE;//否則返回最小數組大小 }
通過源碼我們可以看出:Arraylist在添加元素的時候,首先進行的是範圍檢查,防止其傳入的參數小於0或者超過數組的size大小,再是和默認分配的大小值進行比較,如果大於默認大小就要進行擴容。擴容的時候首先把舊數組的大小提升1.5倍,成為新數組的大小值。同時也可以看到Arrylist的大小邊界是Interger的最大值,這個數字是很大的:2147483647,也就是它的最大值是這麽多,這個值足以我們程序員平常進行使用!在檢查完畢和擴容完畢之後,就是要進行數組的拷貝了,我們開看一下System.arrayCopy()方法的源碼:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
其中可以看出,它是native修飾的,也就是說它是一個引用本地其他語言的代碼庫(windows就是.dll庫)的方法,具體的源碼我們不深究了,這裏解釋一下它的參數,src:你要從哪個數組復制,srcpos:要復制的位置起點 dest:要復制要哪個數組,destPos:要復制到的數組起始位置 lengh:復制的長度
System.arraycopy(elementData, index, elementData, index + 1, size - index);
代入add方法裏,也就是說把elementData的從index開始的元素復制size-index長度到elementData中,從index+1開始。這是一個自我復制的過程,為了保證數據的完整。然後把指定的元素值放入到指定的位置中。我們來用圖解釋一下這個復制的過程:首先是圖1-1表示的是一共有5個元素,那麽這個數組的size就是5.我們現在要在index=2的位置上插入元素六。我們來看看通過arraycopy方法之後的數組是什麽樣的:如圖1-2。然後調用lementData[index] = element方法,之後數組就會變成圖1-3:可見我們要插入的元素六就這樣插入到指定位置上了,從中也可以看出自我復制也就是數組的元素從指定的位置向後移動,這也間接說明了Arraylist插入數據的效率比較低,因為它要移動數據。
圖 1-3
2:remove方法
我們看romove方法首先還是進行範圍檢查,然後用elementData()方法去找到指定數組元素中的值,再用size-index-1轉為numMoved,接著進行再次自我復制。然後把size-1甚至設置為null。這樣這個數組中元素就是null了。
public E remove(int index) {//根據指定位置移除元素 rangeCheck(index);//範圍檢查 modCount++;//修改次數+1 E oldValue = elementData(index);//根據數組元素的位置找到舊值 int numMoved = size - index - 1; if (numMoved > 0)//判斷是否大於0 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // return oldValue;//返回舊值 }
為了更好的理解remove方法,我們來用畫圖的方式理解一下:其中一二三到十使我們存放的值,如圖2-1.那麽這個數組的size=10,假如我們移除remove(3),則index=3,我們可以看到index=2對應的值是四,然後numMoved的值是7,大於0。然後進行自我復制。從elementData的index=3開始,復制之後的結果如圖2-2,可以看出從index開始的位置所有的元素往前移動了一位,這就是arraycopy方法的根本目的,然後再把 elementData[--size] = null; 也就是最後一個元素十置為null,這樣數組的大小就自然減-1,並且順利的移出了指定位置的元素。這就是remove方法原理
圖 2-1
圖 2-2
四:Arraylist的常用方法源碼分析
我們再來看一下Arraylist中一些常見方法的源碼,其中包括返回其大小的方法,判斷是否為空的方法。是否包含某個元素的方法等等,這些方法比較簡單,我只列出了基本的註釋。
public int size() {//取其大小 return size;//返回size的數組 } public boolean isEmpty() {//判斷list是否為空 return size == 0;//用其size和0去比較 } public boolean contains(Object o) {//判斷指定元素是否包含 return indexOf(o) >= 0;//返回indexof方法的返回結果是否大於0。大於0表示含有,如果小於0表示沒有 } public int indexOf(Object o) {//檢索指定元素是否存在 if (o == null) {//如果元素為null for (int i = 0; i < size; i++)//遍歷循環整個數組 if (elementData[i]==null)//如果找到元素為null return i;//返回元素中的位置 } else {//如果不為null for (int i = 0; i < size; i++)//遍歷循環數組 if (o.equals(elementData[i]))//如果指定元素和數組中的元素相同 return i;//返回數組中的位置 } return -1;//否則返回-1 } public int lastIndexOf(Object o) {//從末尾檢索指定元素 if (o == null) {//如果元素為null for (int i = size-1; i >= 0; i--)//按照元素從大到小 if (elementData[i]==null)//如果元素為null return i;//返回數組中的位置 } else {//如果元素不為null for (int i = size-1; i >= 0; i--)//按照元素從大到小 if (o.equals(elementData[i]))//如果元素等於數組中的元素 return i;//返回數組中的位置 } return -1;//否則返回-1 }
public boolean remove(Object o) {//判斷元素是否能夠移除 if (o == null) {//如果元素為null for (int index = 0; index < size; index++)// if (elementData[index] == null) {//如果元素值為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++;//修改次數+1 int numMoved = size - index - 1;//從指定元素的下一個開始 if (numMoved > 0)//如果元素的下一個大於0 System.arraycopy(elementData, index+1, elementData, index, numMoved);//拷貝數組 elementData[--size] = null; //數組對象的最後一個值賦值為null } public void clear() { //清除元素 modCount++;//修改次數+1 for (int i = 0; i < size; i++)//遍歷循環整個數組 elementData[i] = null;//將其值設為null,便於GC回收 size = 0;//size等於0 }
五:Arraylist與linkedlist的區別
1.從本篇博文中可以看出ArrayList基於動態數組的數據結構,但是LinkedList基於鏈表的數據結構。
2:Arraylist是有序的,元素它是按照固定的順序排列,每次都往後移動一位,並且很容易可以看出它是允許值為null。而linkedlist是無序的。
3.對於隨機訪問get和set,ArrayList覺得優於LinkedList,因為LinkedList要移動指針。 我們在這裏看一下get方法的源碼,其中可以看出其只需要做個範圍檢查,然後就可以通過數組的位置計算 處其位置上的值,這也是數組的特點,獲取數組很快。但是linkedlist是鏈表結構的,所謂結構決定功能,鏈表的獲取元素時候需要一層層去節點裏面遍歷,這無疑增加了工作量,所以linked的隨機 訪 問要落後與Arraylist
public E get(int index) {//根據指定位置返回對應的值 rangeCheck(index);//調用範圍檢查的方法,防止其越界 return elementData(index);//把參數傳入,調用elementData()方法 }
4.對於新增和刪除操作add和remove,LinkedList有很大的優勢,因為ArrayList要移動數據
我們通過比對發現add和remove方法,它們都要經過System.arrayCopy()方法,進行數據的移動,比較麻煩,而基於鏈表的結構的LinkedList則只需要進行值和位置的封裝,然後放入鏈表即 可。
5:Arraylist是非線程安全的,其中我們可以看出它並沒有同步機制的實現,也沒有synchronize等關鍵字的修飾。所以在多線程的環境下慎用Arraylist。同時linkedList也並非線程安全的!
六:總結
本篇博文介紹了Arraylist的源碼,從其中可以看出jdk設計者的巧妙,包括其add方法的校驗,防止校驗,還有動態擴容的特性,它作為一個我們在實際開發中常用的數據容器,按照從全局變量到構造方法再到其add、remove方法的學習,我們對arraylist也有了進一步的認知,同時與linkedlist進行了對比,以便於我們更好的掌握Arraylist!好了,本次博客就到這裏結束了。
Java中Arraylist源碼分析