ArrayList原始碼分析

c5p1ng發表於2021-03-07

      序言

          第一次看原始碼,借鑑的是這位博主的文章:http://blog.csdn.net/csh624366188/article/details/6896656  個人覺得他寫的一系列文章很好,稍微有點深度,他是一個農村的,我也是,佩服他的堅持,希望自己也能堅持下去,他寫的這篇文章還是在2011年,那時候他的jdk版本估計也才4到5左右,現在jdk已經到8了,我檢視的原始碼是jdk7,有那麼一點點不同,不過大體上是沒什麼改變的。在閱讀的過程中,自己也慢慢的適應去看api文件,全英文版的,第一次看完了一個方法,花了我大概30分鐘的時間,但是感覺收穫滿滿,下面我會一一介紹的,用的是有道詞典,直接官網可以下載,個人感覺還是很不錯的,有道確實越來越好了,給出網址(http://cidian.youdao.com/?keyfrom=dict2.index),自己又需要的可以去下載。

          這篇文章個人感覺幫助我很多。感興趣的同學也可以去看看。

                                                            ----WH

 


一、檢視原始碼的方法

       自己也是第一次看原始碼,從上面介紹的那位博主哪裡學到的,以後再看原始碼多的話,應該還會滋生出更多的經驗來,設計模式要懂,要知道大概的層次結構。

          1、看繼承結構

               看這個類的層次結構,處於一個什麼位置,可以在自己心裡有個大概的瞭解。

          2、看構造方法

              在構造方法中,看做了哪些事情,跟蹤方法中裡面的方法

            3、看常用的方法

              跟構造方法一樣,這個方法實現功能是如何實現的

 

二、ArrayList原始碼       

      1、繼承結構和層次關係

            首先我們來看一下ArrayList的繼承結構

               ArrayList extends AbstractList

               AbstractList extends AbstractCollection 

               所有類都繼承Object  所以ArrayList的繼承結構就是下圖這樣

               1、為什麼要先繼承AbstractList,而讓AbstractList先實現List<E>?而不是讓ArrayList直接實現List<E>?

                  這裡是有一個思想,介面中全都是抽象的方法,而抽象類中可以有抽象方法,還可以有具體的實現方法,正是利用了這一點,讓AbstractList是實現介面中一些通用的方法,而具體的類,如ArrayList就繼承這個AbstractList類,拿到一些通用的方法,然後自己在實現一些自己特有的方法,這樣一來,讓程式碼更簡潔,就繼承結構最底層的類中通用的方法都抽取出來,先一起實現了,減少重複程式碼。所以一般看到一個類上面還有一個抽象類,應該就是這個作用

             

              

            在來看看ArrayList實現了哪些介面?

                1、List<E>介面:我們會出現這樣一個疑問,在檢視了ArrayList的父類AbstractList也實現了List<E>介面,那為什麼子類ArrayList還是去實現一遍呢?這是想不通的地方,所以我就去查資料,有的人說是為了檢視程式碼方便,使觀看者一目瞭然,說法不一,但每一個讓我感覺合理的,但是在stackOverFlow中找到了答案,這裡其實很有趣,網址貼出來 http://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete開發這個collection 的作者Josh說。這其實是一個mistake,因為他寫這程式碼的時候覺得這個會有用處,但是其實並沒什麼用,但因為沒什麼影響,就一直留到了現在。

                2、RandomAccess介面:這個是一個標記性介面,通過檢視api文件,它的作用就是用來快速隨機存取,有關效率的問題,在實現了該介面的話,那麼使用普通的for迴圈來遍歷,效能更高,例如arrayList。而沒有實現該介面的話,使用Iterator來迭代,這樣效能更高,例如linkedList。所以這個標記性只是為了讓我們知道我們用什麼樣的方式去獲取資料效能更好,可以參考這篇博文,http://blog.csdn.net/keda8997110/article/details/8635005

                3、Cloneable介面:實現了該介面,就可以使用Object.Clone()方法了。

                4、Serializable介面:實現該序列化介面,表明該類可以被序列化,什麼是序列化?簡單的說,就是能夠從類變成位元組流傳輸,然後還能從位元組流變成原來的類。

                

 

      2、構造方法

           三個構造方法

                

 

          1、無參構造方法:(原諒我一直不知道能這樣貼程式碼,搞得之前一直都是發截圖。)             

1    /**
2     * Constructs an empty list with an initial capacity of ten.  這裡就說明了預設會給10的大小,所以說一開始arrayList的容量是10.
3     */
    //ArrayList中儲存資料的其實就是一個資料,這個陣列就是elementData,在123行定義的 private transient Object[] elementData;
4    public ArrayList() {   5 super(); //呼叫父類中的無參構造方法,父類中的是個空的構造方法 6 this.elementData = EMPTY_ELEMENTDATA;//EMPTY_ELEMENTDATA:是個空的Object[], 將elementData初始化,elementData也是個Object[]型別。空的Object[]會給預設大小10,等會會解釋什麼時候賦值的。 7 }

 

         2、有參構造方法1        

 1     /**
 2      * Constructs an empty list with the specified initial capacity.
 3      *
 4      * @param  initialCapacity  the initial capacity of the list
 5      * @throws IllegalArgumentException if the specified initial capacity
 6      *         is negative
 7      */
 8     public ArrayList(int initialCapacity) {
 9         super(); //父類中空的構造方法
10         if (initialCapacity < 0)    //判斷如果自定義大小的容量小於0,則報下面這個非法資料異常
11             throw new IllegalArgumentException("Illegal Capacity: "+
12                                                initialCapacity);
13         this.elementData = new Object[initialCapacity]; //將自定義的容量大小當成初始化elementData的大小
14     }

 

         3、有參構造方法2(不常用)

      

   //這個構造方法不常用,舉個例子就能明白什麼意思
    /*
        Strudent entends Person
         ArrayList<Person>、 Person這裡就是泛型
        我還有一個Collection<Student>、由於這個Student繼承了Person,那麼根據這個構造方法,我就可以把這個Collection<Student>轉換為ArrayList<Sudent>這就是這個構造方法的作用 
    */
     public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();    //轉換為陣列
        size = elementData.length;   //陣列中的資料個數
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class) //每個集合的toarray()的實現方法不一樣,所以需要判斷一下,如果不是Object[].class型別,那麼久需要使用ArrayList中的方法去改造一下。
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }    

   總結一下:arrayList的構造方法就做一件事情,就是初始化一下儲存資料的容器,其實本質上就是一個陣列,在其中就叫elementData。其他什麼也不做了。

    

    

      3、常用方法

            add()方法有兩個

                

              boolean add(E);//預設直接在末尾新增元素。           

   /**
     * Appends the specified element to the end of this list.新增一個特定的元素到list的末尾。
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {    
    //確定內部容量是否夠了,size是陣列中資料的個數,因為要新增一個元素,所以size+1,先判斷size+1的這個個數陣列能否放得下,就在這個方法中去判斷是否陣列.length是否夠用了。
        ensureCapacityInternal(size + 1);  // Increments modCount!!
     //在資料中正確的位置上放上元素e,並且size++
        elementData[size++] = e;
        return true;
    }    

             ensureCapacityInternal(xxx);

   private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) { //看,判斷初始化的elementData是不是空的陣列,也就是沒有長度
    //因為如果是空的話,minCapacity=size+1;其實就是等於1,空的陣列沒有長度就存放不了,所以就將minCapacity變成10,也就是預設大小,但是帶這裡,還沒有真正的初始化這個elementData的大小。
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    //確認實際的容量,上面只是將minCapacity=10,這個方法就是真正的判斷elementData是否夠用
        ensureExplicitCapacity(minCapacity);
    }    

             ensureExplicitCapacity(xxx); 

   private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
//minCapacity如果大於了實際elementData的長度,那麼就說明elementData陣列的長度不夠用,不夠用那麼就要增加elementData的length。這裡有的同學就會模糊minCapacity到底是什麼呢,這裡給你們分析一下

/*第一種情況:由於elementData初始化時是空的陣列,那麼第一次add的時候,minCapacity=size+1;也就minCapacity=1,在上一個方法(確定內部容量ensureCapacityInternal)就會判斷出是空的陣列,就會給
  將minCapacity=10,到這一步為止,還沒有改變elementData的大小,
 第二種情況:elementData不是空的陣列了,那麼在add的時候,minCapacity=size+1;也就是minCapacity代表著elementData中增加之後的實際資料個數,拿著它判斷elementData的length是否夠用,如果length
不夠用,那麼肯定要擴大容量,不然增加的這個元素就會溢位。
*/

if (minCapacity - elementData.length > 0) //arrayList能自動擴充套件大小的關鍵方法就在這裡了 grow(minCapacity); }

      grow(xxx); arrayList核心的方法,能擴充套件陣列大小的真正祕密。

   private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //將擴充前的elementData大小給oldCapacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
        if (newCapacity - minCapacity < 0)//這句話就是適應於elementData就空陣列的時候,length=0,那麼oldCapacity=0,newCapacity=0,所以這個判斷成立,在這裡就是真正的初始化elementData的大小了,就是為10.前面的工作都市準備工作。
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超過了最大的容量限制,就呼叫hugeCapacity,也就是將能給的最大值給newCapacity
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
    //新的容量大小已經確定好了,就copy陣列,改變容量大小咯。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

      hugeCapacity()

 1 //這個就是上面用到的方法,很簡單,就是用來賦最大值。
 2     private static int hugeCapacity(int minCapacity) {
 3         if (minCapacity < 0) // overflow
 4             throw new OutOfMemoryError();
 5 //如果minCapacity都大於MAX_ARRAY_SIZE,那麼就Integer.MAX_VALUE返回,反之將MAX_ARRAY_SIZE返回。因為maxCapacity是三倍的minCapacity,可能擴充的太大了,就用minCapacity來判斷了。
 6 //Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639  也就是說最大也就能給到第一個數值。還是超過了這個限制,就要溢位了。相當於arraylist給了兩層防護。
 7         return (minCapacity > MAX_ARRAY_SIZE) ?
 8             Integer.MAX_VALUE :
 9             MAX_ARRAY_SIZE;
10     }    

 

          void add(int,E);在特定位置新增元素,也就是插入元素

 1   public void add(int index, E element) {
 2         rangeCheckForAdd(index);//檢查index也就是插入的位置是否合理。
 3 
 4 //跟上面的分析一樣,具體看上面
 5         ensureCapacityInternal(size + 1);  // Increments modCount!!
 6 //這個方法就是用來在插入元素之後,要將index之後的元素都往後移一位,
 7         System.arraycopy(elementData, index, elementData, index + 1,
 8                          size - index);
 9 //在目標位置上存放元素
10         elementData[index] = element;
11         size++;//size增加1
12     }

          rangeCheckForAdd(index)

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)   //插入的位置肯定不能大於size 和小於0
//如果是,就報這個越界異常
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

          System.arraycopy(...):就是將elementData在插入位置後的所有元素往後面移一位。檢視api文件

 1 public static void arraycopy(Object src,
 2              int srcPos,
 3              Object dest,
 4              int destPos,
 5              int length)
 6 src:源物件
 7 srcPos:源物件物件的起始位置
 8 dest:目標物件
 9 destPost:目標物件的起始位置
10 length:從起始位置往後複製的長度。
11 
12 //這段的大概意思就是解釋這個方法的用法,複製src到dest,複製的位置是從src的srcPost開始,到srcPost+length-1的位置結束,複製到destPost上,從destPost開始到destPost+length-1的位置上,
13 Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. A subsequence of array components are copied from 
the source array referenced by src to the destination array referenced by dest. The number of components copied is equal to the length argument. The components at positions srcPos through srcPos+length-1
in the source array are copied into positions destPos through destPos+length-1, respectively, of the destination array. 14 15 //告訴你複製的一種情況,如果A和B是一樣的,那麼先將A複製到臨時陣列C,然後通過C複製到B,用了一個第三方引數 16 If the src and dest arguments refer to the same array object, then the copying is performed as if the components at positions srcPos through srcPos+length-1 were first copied to
a temporary array with length components and then the contents of the temporary array were copied into positions destPos through destPos+length-1 of the destination array. 17 18 19 //這一大段,就是來說明會出現的一些問題,NullPointerException和IndexOutOfBoundsException 還有ArrayStoreException 這三個異常出現的原因。 20 If dest is null, then a NullPointerException is thrown. 21 22 If src is null, then a NullPointerException is thrown and the destination array is not modified. 23 24 Otherwise, if any of the following is true, an ArrayStoreException is thrown and the destination is not modified: 25 26 The src argument refers to an object that is not an array. 27 The dest argument refers to an object that is not an array. 28 The src argument and dest argument refer to arrays whose component types are different primitive types. 29 The src argument refers to an array with a primitive component type and the dest argument refers to an array with a reference component type. 30 The src argument refers to an array with a reference component type and the dest argument refers to an array with a primitive component type. 31 Otherwise, if any of the following is true, an IndexOutOfBoundsException is thrown and the destination is not modified: 32 33 The srcPos argument is negative. 34 The destPos argument is negative. 35 The length argument is negative. 36 srcPos+length is greater than src.length, the length of the source array. 37 destPos+length is greater than dest.length, the length of the destination array. 38 39 40 //這裡描述了一種特殊的情況,就是當A的長度大於B的長度的時候,會複製一部分,而不是完全失敗。 41 Otherwise, if any actual component of the source array from position srcPos through srcPos+length-1 cannot be converted to the component type of the destination array by assignment conversion, an ArrayStoreException is thrown.
In this case, let k be the smallest nonnegative integer less than length such that src[srcPos+k] cannot be converted to the component type of the destination array; when the exception is thrown, source array components from positions
srcPos through srcPos+k-1 will already have been copied to destination array positions destPos through destPos+k-1 and no other positions of the destination array will have been modified. (Because of the restrictions already itemized,

this paragraph effectively applies only to the situation where both arrays have component types that are reference types.) 42 43 //這個引數列表的解釋,一開始就說了, 44 Parameters: 45 src - the source array. 46 srcPos - starting position in the source array. 47 dest - the destination array. 48 destPos - starting position in the destination data. 49 length - the number of array elements to be copied.

  

        刪除方法有5個

            挑幾個特點的講解。其實都類似,其中fastRemove(int)方法是private的,是提供給remove(Object)這個方法用的。

            

            remove(int):通過刪除指定位置上的元素

    public E remove(int index) {
        rangeCheck(index);//檢查index的合理性

        modCount++;//這個作用很多,比如用來檢測快速失敗的一種標誌。什麼是快速失敗請看我的每日面試題四中的解答。
        E oldValue = elementData(index);//通過索引直接找到該元素

        int numMoved = size - index - 1;//計算要移動的位數。
        if (numMoved > 0)
//這個方法也已經解釋過了,就是用來移動元素的。
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
//將--size上的位置賦值為null,讓gc(垃圾回收機制)更快的回收它。
        elementData[--size] = null; // clear to let GC do its work
//返回刪除的元素。
        return oldValue;
    }

 

            remove(Object):這個方法可以看出來,arrayList是可以存放null值得。

  //感覺這個不怎麼要分析吧,都看得懂,就是通過元素來刪除該元素,就依次便利,如果有這個元素,就將該元素的索引傳給fastRemobe(index),使用這個方法來刪除該元素,
//fastRemove(index)方法的內部跟remove(index)的實現幾乎一樣,這裡最主要是知道arrayList可以儲存null值
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; }

          clear():將elementData中每個元素都賦值為null,等待垃圾回收將這個給回收掉,所以叫clear

    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

          removeAll(collection c):

1     public boolean removeAll(Collection<?> c) {
2         return batchRemove(c, false);//批量刪除
3     }

          batchRemove(xx,xx):用於兩個方法,一個removeAll():它只清楚指定集合中的元素,retainAll()用來測試兩個集合是否有交集。

//這個方法,用於兩處地方,如果complement為false,則用於removeAll如果為true,則給retainAll()用,retainAll()是用來檢測兩個集合是否有交集的。
   private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData; //將原集合,記名為A
        int r = 0, w = 0;   //r用來控制迴圈,w是記錄有多少個交集
        boolean modified = false;  
        try {
            for (; r < size; r++)
//引數中的集合C一次檢測集合A中的元素是否有,
                if (c.contains(elementData[r]) == complement)
//有的話,就給集合A
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
//如果contains方法使用過程報異常
            if (r != size) {
//將剩下的元素都賦值給集合A,
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
//這裡有兩個用途,在removeAll()時,w一直為0,就直接跟clear一樣,全是為null。
//retainAll():沒有一個交集返回true,有交集但不全交也返回true,而兩個集合相等的時候,返回false,所以不能根據返回值來確認兩個集合是否有交集,而是通過原集合的大小是否發生改變來判斷,如果原集合中還有元素,則代表有交集,而元集合沒有元素了,說明兩個集合沒有交集。
                // 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;
    }

 

      4、總結

     這篇文章花費了我一上午的時間,真的很累,但是也很有收穫,完全清楚了arrayList的實現,和它的一些重要的方法的原理

        1、arrayList可以存放null。

        2、arrayList本質上就是一個elementData陣列。

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

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

        5、arrayList由於本質是陣列,所以它在資料的查詢方面會很快,而在插入刪除這些方面,效能下降很多,有移動很多資料才能達到應有的效果

        6、arrayList實現了RandomAccess,所以在遍歷它的時候推薦使用for迴圈。

 

     希望大家看到這篇文章能有所幫助和提高。那將是對我最大的安慰。該吃午飯了。

 

 

 

 

 

 

 

 

 

 

        

相關文章