1. 程式人生 > >Java中Arraylist源碼分析

Java中Arraylist源碼分析

如何 and 檢索 介紹 包括 com 這就是 pac 程序

前言: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源碼分析