【Java集合】系列一:詳解ArrayList的底層原理(本篇源碼基於Java11)

引言

ArrayList集合類在面試、開發中飽受關注,用起來也是真香。本篇文章有針對性的歸納整理ArrayList的常見問題,如有遺漏,歡迎留言或評論。

面試開始

小夥子,說下ArrayList的底層數據結構吧?

ArrayList的底層數據結構就是一個數組,數組元素的類型爲Object類型,對ArrayList的所有操作底層都是基於該數組的。

程序清單1: ArrayList的底層數組

transient Object[] elementData; 

由源碼可以看出,底層是個Object類型的數組,並且是用關鍵字transient修飾的,表示數組不可被序列化

那結合剛剛說的底層數組實現說下ArrayList有哪些優缺點?

  • ArrayList的優點
  1. ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快。
  2. ArrayList在順序添加一個元素的時候非常方便,只是往數組裏面添加了一個元素而已。
  3. 根據下標遍歷、訪問元素,效率高。
  4. 可以自動擴容,默認爲每次擴容爲原來的1.5倍。
  • ArrayList的缺點
  1. 插入和刪除元素的效率較低。
  2. 根據元素下標查找元素需要遍歷整個元素數組,效率不高。
  3. 線程不安全。

好,剛剛你有提到擴容,可以說下ArrayList的擴容機制嗎?

主要從構造函數和add()方法擴容兩個層面進行分析。

程序清單2: ArrayList的構造函數源碼

   private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
   
   public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

由上述源碼可發現,當用無參構造初始化ArrayList時,默認初始化爲一個空的數組。

程序清單3: ArrayList的擴容源碼

   public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    
    private Object[] grow() {
        return grow(size + 1);
    }
    
    /* 擴容代碼 */
    private Object[] grow(int minCapacity) {
        /* 集合擴容完成後,需要將舊集合中的元素全部複製到新集合中 */
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
    
    /* 新的容量 */
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        /* 擴容爲1.5倍 */
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                /* 返回默認大小(10)和擴容後的大小的最大值 */
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

由上述源碼可發現,當添加元素時每次都會校驗數組大小。當初始化一個空的集合時,第一次add元素時集合的大小會被初始化爲10。然後隨着集合元素不斷增加,當第11個元素插入時,這個時候集合需要擴容,擴容後的容量就是10+10>>1=15。擴容完成後,需要將員集合的元素複製到新的集合中。

注意:根據以上源碼分析可以知道,ArrayList每次擴容都會在堆內存裏開闢一個新的集合空間,將舊的集合中的所有元素都拷貝到新的集合中,舊的集合等待JVM回收。實際上這種不斷複製需要內存和時間開銷,所以最好在ArrayList初始化的時候就指定容量,並儘量保證之後不會擴容。

ArrayList是線程安全的麼?爲什麼?

ArrayList是線程不安全的。體現在add()方法和迭代器裏,具體的這裏將結合源碼說明。

來呀,先給國師上源碼:

程序清單4: ArrayList的add()源碼

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

注意看modCount++(該成員變量記錄着ArrayList的修改次數)這裏,我們知道自增、自減操作都是非原子操作,併發條件下必然有安全性問題。

add(e, elementData, size)這個方法的作用就是將當前的新元素加到列表後面,ArrayList底層數組的大小是否滿足,如果size 長度等於底層數組的長度,那麼就要對這個數組進行擴容。注意看,這個方法是沒有任何的線程安全性保障的,假設現在ArrayList只剩一個元素可以添加了,此時線程1判斷無需擴容進行正常添加操作;當添加尚未完成時,線程2也進入到這裏,進行判斷,也是返回還剩一個元素可以添加(但實際上這個位置已經被線程1佔用了),此時線程2操作會報數組越界異常:ArrayIndexOutOfBoundsException。

程序清單5: ArrayList的迭代器源碼

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                SubList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = root.modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
        final void checkForComodification() {
            if (root.modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

注意看checkForComodification()方法的實現,會對ArrayList的modCount參數(該成員變量記錄着ArrayList的修改次數)進行判斷,如果實際修改次數和預期修改次數expectedModCount不一致(併發條件下會出現),則會拋出併發修改異常ConcurrentModificationException。

怎樣讓ArrayList成爲線程安全的呢?

有兩種方案:

  1. 使用synchronized關鍵字
  2. 創建ArrayList對象的時候採用Collections類中的靜態方法synchronizedList(new ArrayList<>())

ArrayList、Vector和LinkedList的區別?

數據結構 線程安全性 增刪改查的效率
ArrayList 數組 線程不安全 查詢快,增刪慢(在末尾增刪除外)
LinkedList 雙向鏈表 線程不安全 查詢慢,增刪快
Vector 數組 線程安全 查詢快,增刪慢(在末尾增刪除外)

面試結束

好的,小夥子表現不錯,對ArrayList的掌握較好,下輪面試再見!悄悄給你透露一下哦,下一輪面試官喜歡考察HashSet,回去好好備戰!

好嘞,謝謝面試官的提醒,下一回合我也會再接再厲。

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