1. 程式人生 > >JAVA常用集合原始碼分析:ArrayList

JAVA常用集合原始碼分析:ArrayList

ArrayList簡介

ArrayList 是一個動態陣列,所謂動態,是相對陣列來說的,我們知道當我們在使用陣列的時候必須指定大小,而且大小隻能是固定的,有時候就很不方便,讓人不爽。而我們的ArrayList恰恰解決了這一痛點,讓我們可以不受束縛地使用陣列。

閱讀方法

  • 看繼承結構與實現介面。 看這個類的層次結構,處於一個什麼位置,可以在自己心裡有個大概的瞭解。
  • 看構造方法 。在構造方法中,看做了哪些事情,跟蹤方法中裡面的方法
  • 看常用的方法。跟構造方法一樣,這個方法實現功能是如何實現

實現了哪些介面

1、List<E>介面:....
2、RandomAccess介面:這個是一個標記性介面,它的作用就是用來快速隨機存取,有關效率的問題,在實現了該介面的話,那麼使用普通的for迴圈來遍歷,效能更高,例如arrayList。而沒有實現該介面的話,使用Iterator來迭代,這樣效能更高,例如linkedList。所以這個標記性只是為了讓我們知道我們用什麼樣的方式去獲取資料效能更好
3、Cloneable介面:實現了該介面,就可以使用Object.Clone()方法了。
4、Serializable介面:實現該序列化介面,表明該類可以被序列化,什麼是序列化?簡單的說,就是能夠從類變成位元組流傳輸,然後還能從位元組流變成原來的類。

成員變數

     //預設大小
    private static final int DEFAULT_CAPACITY = 10;

    //暫時不知道有什麼用,只知道是個空陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //暫時不知道有什麼用,只知道是個空陣列
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //儲存資料的陣列,從這裡可以看出,arrayList底層是由陣列實現的,transient表示不能被序列化
    transient Object[] elementData; // non-private to simplify nested class access

    //ArrayList元素的個數
    private int size;

從這幾個屬性中,我們可以知道一下兩點

預設容量是10,也就是我們new 一個ArrayList時不指定大小的話,預設就是10

底層是由一個數組來負責儲存資料的

至於EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA這兩個類成員,我們暫時不知道它們的作用

建構函式

    //指定大小 
    public ArrayList(int initialCapacity) 
 
    //不傳指定大小的話,就用預設大小
    public ArrayList()

    //傳一個集合物件
    public ArrayList(Collection<? extends E> c)
        
  

這三個建構函式都沒什麼特別的,都是我們平時用的比較多的,值得一提的是,前面我們提到的EMPTY_ELEMENTDATA 在建構函式的實現中出現了

根據程式碼,我們可以發現當容量為0(指定的大小為0或者傳入的集合大小為0),就把我們存放資料的底層陣列指向EMPTY_ELEMENTDATA這麼一個空陣列,當時無參構造的時候,底層陣列指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA由此可見,這兩個空陣列的作用是標記底層陣列的初始化方式(個人見解,勿噴~)

接下來我們再來看我們常用方法的原始碼

1.add(Object) 在末尾增加元素

//把一個物件加入ArrayList末尾
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

這裡我們看見了一個會讓我們產生疑問的程式碼,就是 ensureCapacityInternal(size + 1) 這行程式碼,按理說,我們底層是陣列實現,只要執行 elementData[size++] = e 就應該是完事了的,那麼 ensureCapacityInternal()的作用是什麼呢?根據方法名字,我們也能猜到,該方法是用來進行進行擴容相關的。我們在一開始也已經提到,雖然ArrayList號稱動態陣列,但是它底層依舊是陣列實現的,陣列是有固定大小的,所以我們在使用的時候底層必須實現一系列的擴容機制。

我們來看看該方法的具體實現

 private void ensureCapacityInternal(int minCapacity) { //minCapacity究竟是啥?
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果資料為空的話
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //此處相當於把minCapacity 設定為預設值10
        }

        //確認實際的容量,上面只是將minCapacity=10,這個方法就是真正的判斷elementData是否夠用
        ensureExplicitCapacity(minCapacity);
    }

在這個方法中,我們首先要明確minCapacity是個什麼樣的變數,有何意義。根據字面意思,它表示的是最小容量,回到我們呼叫該方法的時候,給它傳的是 size+1 ,也就是說 minCapacity = size + 1,也表示完我們執行完add後預期的陣列大小,如果陣列的最大容量比這個小,說明無法裝下,就要進行擴容操作。

一開始判斷elementData是否為空,如果為空的話,顯然空的陣列無法新增物件,所以就把預設擴容大小設為預設大小(10)

在該方法中,又呼叫了一個ensureExplicitCapacity(minCapacity);方法,該方法傳入minCapacity,我們接著看這個方法的原始碼

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//應該是一個修改標誌位?

        //當底層陣列的大小比預期最小容量還小的時候,就必須進入擴容操作啦
        //這裡又包含了兩種情況
        //1.底層陣列為空,將要新增的元素為第一個元素,此時minCapacity為1,經過一輪判斷(上一個方法中),被設為10
        //2.底層陣列不為空,此時minCapacity為size+1, 將之與底層資料大小進行比較,如果底層陣列大小比他小,進行擴容

        if (minCapacity - elementData.length > 0)
            //執行擴容操作
            grow(minCapacity);
    }

在該方法中,呼叫了我們整個ArrayList的核心擴容程式碼 grow(),我們來看看它的原始碼

private void grow(int minCapacity) {
        //把當前底層陣列大小賦給舊值
        int oldCapacity = elementData.length;
        //擴容為原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果擴容後的值依然達不到預期值,就把新值設為預期值(add後的容量)
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //擴容後的值超過了允許的最大值,就把能給的最大值給newCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //複製一個新的陣列
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

 

由此,我們的add,操作便完成啦,核心便是擴容機制的實現。

2.add(int , E) 在指定位置增加元素

  public void add(int index, E element) {
        // 判斷是否越界
        rangeCheckForAdd(index);
        
        //容量判斷以及擴容處理
        ensureCapacityInternal(size + 1);  // Increments modCount!!

        //這個方法就是用來在插入元素之後,要將index之後的元素都往後移一位,
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

rangeCheckForAdd的具體實現如下

private void rangeCheckForAdd(int index) {
        //索引大於陣列大小,或者小於0,即越界,丟擲越界異常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

至於擴容,在上頭已經分析過了,這裡不做贅述。

3.remove(int index) 按下標刪除

public E remove(int index) {
        //檢查下標是否合法
        rangeCheck(index);

        modCount++;
        //儲存舊值
        E oldValue = elementData(index);
        
        //移動的位數
        int numMoved = size - index - 1;

        //把後面的往前移一位,這裡用了個arraycopy方法,把 index+1後面的numMoved位複製到從index位開始的..
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);

        //置空,讓GC處理它
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
}

這裡我們碰到了System.arraycopy()這個方法,在前面我們也碰到過,這是方法主要用於陣列複製,引數如下

public static void arraycopy(Object src, //src:源物件
             int srcPos, //源物件物件的起始位置
             Object dest, //目標物件
             int destPos,  //目標物件的起始位置
             int length)  // 從起始位置往後複製的長度

4.remove(Object)   //刪除該元素在陣列中第一次出現的位置上的資料。 如果有該元素返回true,如果false。

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;
    }

其實邏輯都不難,值得注意的是這個方法是可以接收null作為引數的,至於fastRemove(),內部實現幾乎和remove(index)一樣..唯一的不同應該是不用判斷越界

5.removeAll() 批量刪除

 public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c); // 判斷是否為空,空的話扔出異常
        return batchRemove(c, false);
    }

這裡簡要介紹下 Objects.requireNonNull() ,首先Objects是java7新增的一個工具類,注意要和Object進行區別。該方法的具體實現如下,主要作用就是判空。

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;

接下來我們就要繼續研究batchRemove()這個方法了

//這個方法,用於兩處地方,如果complement為false,則用於removeAll如果為true,則給retainAll()用,retainAll()是用來檢測兩個集合是否有交集的。
private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData; //拷貝下資料部分
        int r = 0, w = 0; //r用來控制迴圈,w是記錄有多少個交集 或者說 代表批量刪除後 陣列還剩多少元素
        boolean modified = false; //標誌位,未修改
        try {
            //高效的儲存兩個集合公有元素的演算法
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement) //如果c不包含
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.

            if (r != size) { //出現異常會導致 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.還有些clear,cotians,size等就不一一看原始碼啦,也沒啥難度,主要就是要掌握擴容機制啦,還有最後的那個批量刪除,也有一丟丟難度,其他的不多研究哦。

總結

arrayList區別於陣列的地方在於能夠自動擴充套件大小,其中關鍵的方法就是gorw()方法。

arrayList中removeAll(collection c)和clear()的區別就是removeAll可以刪除批量指定的元素,而clear是全是刪除集合中的元素。

增刪改查中, 增導致擴容,則會修改modCount,刪一定會修改。 改和查一定不會修改modCount。

想說的話

(⊙o⊙)…這是我第一次研究原始碼....一開始我是拒絕的.密密麻麻的程式碼,還有令人頭大的英文註釋,啊!我要暈啦,不過想到自己還這麼菜,於是就硬著頭皮啃啦,好在ArrayList的實現並不是很複雜,所以第一次原始碼閱讀雖然比較慢,但最後還是搞的差不多啦。同時,這也是我寫的最久的一篇博文....所以增加了這麼寫廢話,哈哈