JAVA容器-自問自答學ArrayList

前言

在之前的幾篇文章里面,我主要都是推薦了一些工具類,為的就是讓大家可以提高開發效率,但是我們在提高開發效率,也應該提高代碼的執行效率,注重代碼的質量。如何提高,其中的一個好辦法就是閱讀源碼,知其然知其所以然。

下面我就以面試問答的形式學習我們的最常用的裝載容器——ArrayList(源碼分析基于JDK8)

問答內容

1.

問:ArrayList有用過嗎?它是一個什么東西?可以用來干嘛?

答:有用過,ArrayList就是數組列表,主要用來裝載數據,當我們裝載的是基本類型的數據int,long,boolean,short,byte...的時候我們只能存儲他們對應的包裝類,它的主要底層實現是數組Object[] elementData。與它類似的是LinkedList,和LinkedList相比,它的查找和訪問元素的速度較快,但新增,刪除的速度較慢。

示例代碼:

        // 創建一個ArrayList,如果沒有指定初始大小,默認容器大小為10
        ArrayList<String> arrayList = new ArrayList<String>();
        // 往容器里面添加元素
        arrayList.add("張三");
        arrayList.add("李四");
        arrayList.add("王五");
        // 獲取index下標為0的元素      張三
        String element = arrayList.get(0);
        // 刪除index下標為1的元素      李四
        String removeElement = arrayList.remove(1);
ArrayList底層實現示意圖
2.

問:您說它的底層實現是數組,但是數組的大小是定長的,如果我們不斷的往里面添加數據的話,不會有問題嗎?

答:ArrayList可以通過構造方法在初始化的時候指定底層數組的大小。

  • 通過無參構造方法的方式ArrayList()初始化,則賦值底層數組Object[] elementData為一個默認空數組Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以數組容量為0,只有真正對數據進行添加add時,才分配默認DEFAULT_CAPACITY = 10的初始容量。
    示例代碼:
    // 定義ArrayList默認容量為10
    private static final int DEFAULT_CAPACITY = 10;

    // 空數組,當調用無參構造方法時默認復制這個空數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正保存數據的底層數組
    transient Object[] elementData; 

    // ArrayList的實際元素數量
    private int size;

    public ArrayList() {
        // 無參構造方法默認為空數組
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  • 通過指定容量初始大小的構造方法方式ArrayList(int initialCapacity)初始化,則賦值底層數組Object[] elementData為指定大小的數組this.elementData = new Object[initialCapacity];
    示例代碼:
    private static final Object[] 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);
        }
    }
  • 當我們添加的元素數量已經達到底層數組Object[] elementData的上限時,我們再往ArrayList元素,則會觸發ArrayList的自動擴容機制,ArrayList會通過位運算int newCapacity = oldCapacity + (oldCapacity >> 1);以1.5倍的方式初始化一個新的數組(如初始化數組大小為10,則擴容后的數組大小為15),然后使用Arrays.copyOf(elementData, newCapacity);方法將原數據的數據逐一復制到新數組上面去,以此達到ArrayList擴容的效果。雖然,Arrays.copyOf(elementData, newCapacity);方法最終調用的是native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)是一個底層方法,效率還算可以,但如果我們在知道ArrayList想裝多少個元素的情況下,卻沒有指定容器大小,則就會導致ArrayList頻繁觸發擴容機制,頻繁進行底層數組之間的數據復制,大大降低使用效率。
    示例代碼:
    public boolean add(E e) {
        //確保底層數組容量,如果容量不足,則擴容
        ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        return true;
    }

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

        // 容量不足,則調用grow方法進行擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * 擴容方法(重點)
     */
    private void grow(int minCapacity) {
        // 獲得原容量大小
        int oldCapacity = elementData.length;
        // 新容量為原容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 再判斷新容量是否已足夠,如果擴容后仍然不足夠,則復制為最小容量長度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 判斷是否超過最大長度限制
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 將原數組的數據復制至新數組, ArrayList的底層數組引用指向新數組
        // 如果數據量很大,重復擴容,則會影響效率
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  • 因此,在我們使用ArrayList的時候,如果知道最終的存儲容量capacity,則應該在初始化的時候就指定ArrayList的容量ArrayList(int initialCapacity),如果初始化時無法預知裝載容量,但在使用過程中,得知最終容量,我們可以通過調用ensureCapacity(int minCapacity)方法來指定ArrayList的容量,并且,如果我們在使用途中,如果確定容量大小,但是由于之前每次擴容都擴充50%,所以會造成一定的存儲空間浪費,我們可以調用trimToSize()方法將容器最小化到存儲元素容量,進而消除這些存儲空間浪費。例如:我們當前存儲了11個元素,我們不會再添加但是當前的ArrayList的大小為15,有4個存儲空間沒有被使用,則調用trimToSize()方法后,則會重新創建一個容量為11的數組Object[] elementData,將原有的11個元素復制至新數組,達到節省內存空間的效果。
    示例代碼:
    /**
     * 將底層數組一次性指定到指定容量的大小
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table 
             ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    /**
     * 將容器最小化到存儲元素容量
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
3.

問:那它是怎么樣刪除元素的?您上面說到ArrayList訪問元素速度較快,但是新增和刪除的速度較慢,為什么呢?

答:

  • 通過源碼我們可以得知,ArrayList刪除元素時,先獲取對應的刪除元素,然后把要刪除元素對應索引index后的元素逐一往前移動1位,最后將最后一個存儲元素清空并返回刪除元素,以此達到刪除元素的效果。

  • 當我們通過下標的方式去訪問元素時,我們假設訪問一個元素所花費的時間為K,則通過下標一步到位的方式訪問元素,時間則為1K,用“大O”表示法表示,則時間復雜度為O(1)。所以ArrayList的訪問數據的數據是比較快的。

  • 當我們去添加元素add(E e)時,我們是把元素添加至末尾,不需要移動元素,此時的時間復雜度為O(1),但我們把元素添加到指定位置,最壞情況下,我們將元素添加至第一個位置add(int index, E element),則整個ArrayList的n-1個元素都要往前移動位置,導致底層數組發生n-1次復制。通常情況下,我們說的時間復雜度都是按最壞情況度量的,此時的時間復雜度為O(n)。刪除元素同理,刪除最后一個元素不需要移動元素,時間復雜度為O(1),但刪除第一個元素,則需要移動n-1個元素,最壞情況下的時間復雜度也是O(n)。

  • 所以ArrayList訪問元素速度較快,但是新增和刪除的速度較慢。

示例代碼:

    /**
     * 將元素添加至末尾
     */
    public boolean add(E e) {
        // 確保底層數組容量,如果容量不足,則擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 將元素添加至指定下標位置
     */
    public void add(int index, E element) {
         // 檢查下標是否在合法范圍內
        rangeCheckForAdd(index);
        // 確保底層數組容量,如果容量不足,則擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 將要添加的元素下標后的元素通過復制的方式逐一往后移動,騰出對應index下標的存儲位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 將新增元素存儲至指定下標索引index
        elementData[index] = element;
        // ArrayList的大小 + 1
        size++;
    }

    /**
     * 通過下標索引的方式刪除元素
     */
    public E remove(int index) {
        // 檢查下標是否在合法范圍內
        rangeCheck(index);

        modCount++;
        // 直接通過下標去訪問底層數組的元素
        E oldValue = elementData(index);

        // 計算數組需要移動的元素個數
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 將要刪除的元素下標后的元素通過復制的方式逐一往前移動
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        //將底層數組長度減1,并清空最后一個存儲元素。
        elementData[--size] = null; // clear to let GC do its work
        // 返回移除元素
        return oldValue;
    }
4.

問:ArrayList是線程安全的嗎?

答:ArrayList不是線程安全的,如果多個線程同時對同一個ArrayList更改數據的話,會導致數據不一致或者數據污染。如果出現線程不安全的操作時,ArrayList會盡可能的拋出ConcurrentModificationException防止數據異常,當我們在對一個ArrayList進行遍歷時,在遍歷期間,我們是不能對ArrayList進行添加,修改,刪除等更改數據的操作的,否則也會拋出ConcurrentModificationException異常,此為fail-fast(快速失敗)機制。從源碼上分析,我們在add,remove,clear等更改ArrayList數據時,都會導致modCount的改變,當expectedModCount != modCount時,則拋出ConcurrentModificationException。如果想要線程安全,可以考慮使用Vector、CopyOnWriteArrayList。

示例代碼:

    /**
     * AbstractList.Itr 的迭代器實現
     */
    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
        //期望的modCount
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                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();
        }
    }

總結

  1. 如果在初始化的時候知道ArrayList的初始容量,請一開始就指定容量ArrayList<String> list = new ArrayList<String>(20);,如果一開始不知道容量,中途才得知,請調用list.ensureCapacity(20);來擴充容量,如果數據已經添加完畢,但仍需要保存在內存中一段時間,請調用list.trimToSize()將容器最小化到存儲元素容量,進而消除這些存儲空間浪費。

  2. ArrayList是以1.5倍的容量去擴容的,如初始容量是10,則容量依次遞增擴充為:15,22,33,49。擴容后把原始數據從舊數組復制至新數組中。

  3. ArrayList訪問元素速度較快,下標方式訪問元素,時間復雜度為O(1),添加與刪除速度較慢,時間復雜度均為O(n)。

  4. ArrayList不是線程安全的,但是在發生并發行為時,它會盡可能的拋出ConcurrentModificationException,此為fail-fast機制。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容