ArrayList源碼分析(jdk 1.8)

寫在前面

ArrayList相信大家做開發的同學都不陌生,在開發過程中這應該是最常用的數據結構了吧。但是現在是“源碼時代”,會用還不夠,要知道他的實現原理,本文主要基於jdk1.8對ArrayList源碼進行分析。

 

一、從主要字段開始

值得注意的是,ArrayList內部會有一個modCount字段,但是這個字段是在父類AbstractList中的,代表着修改次數,後面會講

/**
     * 默認容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空元素集,構造函數傳入空集合的時候使用
    */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 空元素集,默認無參構造函數中使用
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
        ArrayList真正保存元素的數組,在第一次插入元素的時候擴容
     * 
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 當前數組元素個數
     *
     * @serial
     */
    private int size;

接下來看一下主要的構造方法:

/**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    //這個是帶數組容量的構造函數
    public ArrayList(int initialCapacity) {
        //如果初始容量大於0,則初始化底層元素數組
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果等於0,則將上面的空數組賦值
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果是小於0的負數,拋非法參數異常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        //默認無參構造給的是一個空數組,不在這個時候擴容
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    //傳入集合的構造函數
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        //如果長度不爲0,進此邏輯
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //如果該元素數組類型不是等於Object[],則拷貝一份Object[]類型的數組 並賦值給
            //elementData
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //長度是0,給空數組
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

二、add方法

1、list默認的add方法:

public boolean add(E e)

/**
     * Appends the specified element to the end of this 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) {
        //好這邊可以看到,在添加元素前搞了一個這個不可描述的動作,從之前的構造方法我們可以看出,
        //一開始數組根本沒有初始化,所以擴容動作必定在這個函數裏做的
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //這裏添加元素
        elementData[size++] = e;
        return true;
    }

來看一下那個函數

private void ensureCapacityInternal(int minCapacity) {
        //調用了這個函數,記住這個minCapacity代表啥:size+1
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

再來看一下參數裏面這個calculateCapacity函數

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果元素數組爲空數組
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //返回默認容量和size+1中較大的那個
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //如果不是空數組,返回size+1
        return minCapacity;
    }

繼續跟蹤:

private void ensureExplicitCapacity(int minCapacity) {
        //首先modCount加1,這個是幹嘛用的後面會說
        modCount++;

        // overflow-conscious code
        //關鍵點來了,這句話啥意思?size+1比當前數組容量大的時候,很明顯剛開始是0,1比0大很正常
        if (minCapacity - elementData.length > 0)
            //grow方法明顯是擴容的真正方法
            grow(minCapacity);
    }

繼續來看grow函數:

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        //獲取數組原始容量
        int oldCapacity = elementData.length;
        //注意這行,擴容後的新容量等於老容量右移一位加上自身,也就是原來的1.5倍,而不是hashmap的        
          //兩倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新容量還是比size+1小,這是什麼情況?想想
        //當然是老容量爲0的時候,0*1.5=0,所以這個時候新容量等於1
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //當擴容之後的新容量比這個整形最大值-8還要大的時候
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //這個方法我就下面會貼出來
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //擴容成新數組
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

來看一下上面的hugeCapacity方法

private static int hugeCapacity(int minCapacity) {
        //如果size+1之後比0小,也就是整形溢出的時候,直接拋異常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //否則如果比整形最大值-8大 就取整形最大值Integer.MAX_VALUE,否則取整形最大值-8
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

至此整個擴容操作完成。

2、指定位置添加元素add方法

public void add(int index, E element)

    public void add(int index, E element) {
        //這個函數很簡單主要是檢查index下標是否越界,否則拋異常
        rangeCheckForAdd(index);
        //這個函數上面分析過了,不再解釋
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //這個函數可能大家沒見過,他的作用是將elementData數組的index位置開始size -index長度的        
       //元素全部拷貝到elementData 數組index+1的位置,也就是將index位置開始的數組全部後移一      
       //位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //再將插入的元素賦值給index位置
        elementData[index] = element;
        //數組元素加1
        size++;
    }

三、remove方法

1.根據下標刪除的remove方法

public E remove(int index) {
        //檢查下標是否越界
        rangeCheck(index);
        //修改次數加1
        modCount++;
        E oldValue = elementData(index);
        //需要移動多少個元素 如數組有5個元素 0,1,2,3,4,index等於2就是5-2-1=2,需要移動兩        
          //個元素
        int numMoved = size - index - 1;
        //如果移動的元素個數大於0
        if (numMoved > 0)
            //這個函數之前介紹過,之前是移動index之後的元素到index+1,現在恰好相反,把index+1    
             //後面的元素移動到index
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //把最後一個元素置爲null,方便jvm回收,也是上面如果移動的元素個數等於0 ,也就是要刪除的 
        //是最後一個元素時,直接置爲空
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

2.根據元素刪除的remove方法

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 {
         //如果要刪除的元素不等於null
            for (int index = 0; index < size; index++)
                //遍歷過程遇到相等的,則刪除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

來看一下fastRemove方法:

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    //你發現了什麼?哎好像就是走之前根據索引刪除的邏輯 一摸一樣啊
    //但是仔細看 你會發現少了一個下標越界的檢查操作,看頭頂這行註釋,爲什麼?該方法是私有方法
     //調用的地方已經保證index在0到size-1之間,所以這個方法名fastRemove挺有意思
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

三、迭代操作

我不知道你面試的時候,有沒有被問到過,ArrayList在循環的時候,能刪除元素嗎?如果你看過面試題,肯定知道答案是不行,那我們來看看爲啥不行。這就和上面頻繁出現的modCount有關了。

首先我們要知道ArrayList有iterator()方法,這個方法的來源是Collection接口繼承了Iterable接口,實現這個接口說明該類是可迭代的,而iterator方法的返回值 類型Iterator成爲迭代器,是需要自己實現的,這個類似於Comparable和Comparator的關係。

知道了這個,那我們對增強for循環一定不陌生,那憑什麼ArrayList可以用增強 for循環,我們自己的類卻不行?原因是增強for底層用的是迭代器操作,我們只要自己實現Iterable接口,也能使用增強for。

public interface Collection<E> extends Iterable<E>
 public Iterator<E> iterator() {
        return new Itr();
    }

接下來看一下問題關鍵:ArrayList的迭代器實現

 private class Itr implements Iterator<E> {
        //遊標,代表的是當前元素的索引下標
        int cursor;       // index of next element to return
        //最近返回的元素
        int lastRet = -1; // index of last element returned; -1 if no such
        //期待的修改次數,我們發現他在迭代器被創建的時候就已經被指定了
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            //遊標是否已到最大值
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
              //這是關鍵所在,檢查修改次數,不一樣會拋異常,下面方法寫了,而上面我們看到無論是添 
           //加元素還是刪除元素modCount都會加1因此過不了這裏的校驗
            checkForComodification();

            int i = cursor;
            //如果遊標大於數組長度,拋出異常
            if (i >= size)
                throw new NoSuchElementException();
            //這裏可能大家會有疑問,上面不是已經判斷遊標是否越界了嗎怎麼還要判斷
            //其實這邊應該是爲了防止多線程併發修改的情況。比如{1,2,3,4,5}有五個元素,這時候線程 
             //1在遍歷,遊標 剛好在索引4這個位置,線程2此時刪除了一個元素,這個時候數組長度只有4 
            //了,此時顯然取不到那個元素,因此此處直接拋出異常
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
             //遊標+1
            cursor = i + 1;
            //獲取對應位置的元素,並賦值給lastRet
            return (E) elementData[lastRet = i];
        }

        //這是迭代器自己的刪除方法,在迭代中是可用的,我們來看下是爲什麼
        public void remove() {
            //    如果一次都沒迭代過,則拋異常,只要調用了next,lastRet都會有值
            if (lastRet < 0)
                throw new IllegalStateException();
            //檢查狀態
            checkForComodification();

            try {
                //調用arraylist自己的remove方法,哎?那按理說modCount++會拋異常啊,爲啥沒有
                //來看下面代碼
                ArrayList.this.remove(lastRet);
                //next之後遊標會加1,但是我們刪除了元素,所以把遊標重置到當前位置
                cursor = lastRet;
                 //這裏是重置lastRet爲-1,刪除之後必須迭代到下一個
                lastRet = -1;
                //這行代碼你發現了什麼?modCount又賦值給了expectedModCount,所以不會報錯
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            //如果當前的修改次數不等於期待的修改次數,拋出異常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

四、寫在最後

關於ArrayList源碼的解析就先寫到這裏,原創不易,註釋都是一行一行手敲的,轉載請註明出處。歡迎大家進行評論和點贊,有什麼問題也可以加我私人微信:zyj-jy66。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章