集合包系列一 —— ArrayList

本系列文章所描述的所有類或接口都是基于 JDK 1.7的源碼,在其它 JDK 的實現(xiàn)方式中可能會有所不同轮纫。

一、 ArrayList

1. 創(chuàng)建 ArrayList

在 JDK 1.7 中創(chuàng)建 ArrayList 提供了三個構(gòu)造函數(shù)金赦,分別如下:

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        super();   // 調(diào)用的是 AbstractList 空的構(gòu)造函數(shù)
        this.elementData = EMPTY_ELEMENTDATA;   // EMPTY_ELEMENTDATA 的初始值為 Object[] EMPTY_ELEMENTDATA = {}
    }

    /**
     * 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) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     * 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();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

據(jù)此可以看出跷究,ArrayList 采用的是數(shù)組的方式來存放對象跳昼,在沒有指定初始化容量的時候般甲,就分配了一個空的對象數(shù)組,沒有任何對象元素鹅颊,也就是容量為0欣除,沒有分配空間。

2. 插入對象:add(E)

add 方法簡單來看就是將數(shù)組中某元素的值賦值為傳入的對象挪略,但在 add 時有個很明顯的問題是:如果此時數(shù)組滿了,該怎么辦滔岳?

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // DEFAULT_CAPACITY = 10
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }

當調(diào)用 ArrayList 的 add 方法時杠娱,首先基于 ArrayList 中已有的元素數(shù)量加1,產(chǎn)生一個名為 minCapacity 的變量谱煤,然后比較此值和 Object 數(shù)組的大小摊求,如果此值大于 Object 數(shù)組大小值,產(chǎn)生一個新的數(shù)組的容量值刘离,此值的計數(shù)方法為當前數(shù)組大小值*1.5室叉,注意:網(wǎng)上很多人說是新的容量值是原來的1.5倍然后加1,其實這是錯誤的硫惕。茧痕,如果得出的容量值仍然小于 minCapacity ,那么就以 minCapacity 作為新的容量值恼除,在得出這個容量值后踪旷,調(diào)用 Arrays.copyOf 來生成新的數(shù)組對象。如果想調(diào)整容量的增長策略豁辉,可以調(diào)用 ensureCapacity 方法令野。

    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != EMPTY_ELEMENTDATA)
            // any size if real element table
            ? 0
            // larger than default for empty table. It's already supposed to be
            // at default size.
            : DEFAULT_CAPACITY;

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

Arrays.copyOf 的實現(xiàn)方法簡單來說,首先是創(chuàng)建一個新的數(shù)組對象徽级,該數(shù)組對象的類型和之前 ArrayList 中元素的類型是一致的气破,如果是 Object 類型,則直接通過 new Object[newLength] 的方式來創(chuàng)建餐抢;如果不是 Object 類型现使,則通過 Array.newInstance 調(diào)用 native 方法來創(chuàng)建相應類型的數(shù)組低匙,在創(chuàng)建完新的數(shù)組對象后,調(diào)用 System.arraycopy 通過 native 方法將之前數(shù)組中的對象復制到新的數(shù)組中朴下。ArrayList相當于在沒指定initialCapacity時就是會使用延遲分配對象數(shù)組空間努咐,當?shù)谝淮尾迦朐貢r才分配10個對象空間。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

當我們已經(jīng)確定了要插入的對象的數(shù)目(并不是在創(chuàng)建ArrayList之前就知道有多少對象要插入的情況)殴胧,就應該首先調(diào)用ensureCapacity來一次性擴容到可以容得下要插入的對象個數(shù)渗稍,這樣就避免的多次進行數(shù)組拷貝的情況,提高了效率团滥,算是優(yōu)化吧竿屹,當然,我們在創(chuàng)建ArrayList之前就知道有多少對象要插入就使用有參構(gòu)造灸姊。
  在 Collection 中增加對象時拱燃,ArrayList 還提供了 add(int, E) 這樣的方法,允許將元素直接插入指定的 int 位置上力惯,這個方法的實現(xiàn)首先要確保插入的位置是目前的 Array 數(shù)組中存在的碗誉,之后還要確保數(shù)組的容量是夠用的。在完成了這些動作后父晶,和 add(E) 不同的地方就出現(xiàn)了哮缺,它要將當前的數(shù)組對象進行一次復制,即將目前 index 及其后的數(shù)據(jù)都往后挪動一位甲喝,然后才能將指定的 index 位置的賦值為傳入的對象尝苇,可見這種方式要多付出一次復制數(shù)組的代價。

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

除了 add(int, E) 這種方法可將對象插入指定的位置外埠胖,ArrayList 還提供了 set(int, E) 這樣的方法來替換指定位置上的對象糠溜。

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

ArrayList 還對外提供了 addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c) 這樣的方法,其實現(xiàn)方式和 add(E)直撤、add(int, E) 基本類似非竿。

3. 刪除對象:remove(E)

remove 對于集合的性能而言也非常重要,當執(zhí)行此方法時谋竖,ArrayList 首先判斷對象是否為 null汽馋,如為 null,則遍歷數(shù)組中已有值的元素圈盔,并比較其是否為 null豹芯,如為 null,則調(diào)用 fastRemove 來刪除相應位置的對象驱敲。fastRemove 方法的實現(xiàn)方式為將 index 后的對象往前復制一位铁蹈,并將數(shù)組中的最后一個元素的值設(shè)置為 null,即釋放了對此對象的引用众眨;如對象非 null握牧,唯一的不同在于通過 E 的 equals 來比較元素的值是否相同容诬,如相同則認為是需要刪除對象的位置,然后同樣是調(diào)用 fastRemove 來完成對象的刪除沿腰。由此可見調(diào)用 remove 方法一次只會移除一個元素览徒。

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

    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 中還提供了 remove(int) 這樣的方法來刪除指定位置的對象,remove(int) 的實現(xiàn)比 remove(E) 多了一個數(shù)組范圍檢測颂龙,但少了對象位置的查找习蓬,因此性能會更好。

    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);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

4. 獲取單個對象:get(int)

get 傳入的參數(shù)為數(shù)組元素的位置措嵌,因此 ArrayList 僅須先做數(shù)組范圍的檢測躲叼,然后即可直接返回數(shù)組中位于此位置的對象。

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

5. 遍歷對象:iterator()

當每次調(diào)用 iterator 方法時企巢,都會創(chuàng)建一個新的內(nèi)部類 Itr 的實例枫慷。當調(diào)用此實例的 hasNext 方法時,比較當前指向的數(shù)組的位置是否和數(shù)組中已有的元素大小相等浪规,如相等則返回 false或听,否則返回 true。
  當調(diào)用實例的 next 方法時笋婿,首先比較在創(chuàng)建此 Iterator 時獲取的 modCount 與目前的 modCount誉裆,如果這兩個 modCount 不相等,則說明在獲取 next 元素時萌抵,發(fā)生了對于集合大小產(chǎn)生影響(新增、刪除)的動作元镀。當發(fā)生這種情況時绍填,則拋出 ConcurrentModificationException。如果 modCount 相等栖疑,則比較當前的游標讨永,如果當前的游標大于數(shù)組的實際大小,則拋出 NoSuchElementException遇革。如果當前游標大于數(shù)組的長度則拋出 ConcurrentModificationException卿闹。
  注意:返回的 iterator 是 fail-fast 的。fail-fast 也就是“快速失敗”萝快,它是Java 集合的一種錯誤檢測機制锻霎。當多個線程對集合進行結(jié)構(gòu)上的改變的操作時,有可能會產(chǎn)生fail-fast機制揪漩。記住是有可能旋恼,而不是一定。例如:假設(shè)存在兩個線程(線程1奄容、線程2)冰更,線程1通過Iterator在遍歷集合A中的元素产徊,在某個時候線程2修改了集合A的結(jié)構(gòu)(是結(jié)構(gòu)上面的修改,而不是簡單的修改集合元素的內(nèi)容)蜀细,那么這個時候程序就會拋出 ConcurrentModificationException 異常舟铜,從而產(chǎn)生fail-fast機制。

    public Iterator<E> iterator() {
        return new 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
        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();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
fail-fast解決辦法

方案一:在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList奠衔,這樣就可以解決谆刨。但是不推薦,因為增刪造成的同步鎖可能會阻塞遍歷操作涣觉。
  方案二:使用CopyOnWriteArrayList來替換ArrayList痴荐。推薦使用該方案。

6. 判斷對象是否存在:contains(E)

為了判斷 E 在 ArrayList 中是否存在官册,做法為遍歷整個 ArrayList 中已有的元素生兆,如 E 為 null,則直接判斷已有元素是否為 null膝宁,如為 null鸦难,則返回 true;如 E 不為 null员淫,則通過判斷 E.equals 和元素是否相等合蔽,如相等則返回 true。
  indexOf 和 lastIndexOf 是 ArrayList 中用于獲取對象所在位置的方法介返,其中 indexOf 為從前往后尋找拴事,而 lastIndexOf 為從后向前尋找。

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

7. 注意要點

對于 ArrayList 而言圣蝎,最須注意的有以下幾點:

  • ArrayList 基于數(shù)組方式實現(xiàn)刃宵,無容量的限制;
  • ArrayList 在執(zhí)行插入元素時可能要擴容徘公,在刪除元素時并不會減小數(shù)組的容量(如希望相應的縮小數(shù)組容量牲证,可以調(diào)用 ArrayList 的 trimToSize() ),在查找元素時要遍歷數(shù)組关面,對于非 null 的元素采取 equals 的方式尋找暑塑;
  • ArrayList 是非線程安全的钧敞。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贯卦,一起剝皮案震驚了整個濱河市申屹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缩抡,老刑警劉巖辛燥,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡挎塌,警方通過查閱死者的電腦和手機徘六,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榴都,“玉大人待锈,你說我怎么就攤上這事∽旄撸” “怎么了竿音?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拴驮。 經(jīng)常有香客問我春瞬,道長,這世上最難降的妖魔是什么套啤? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任宽气,我火速辦了婚禮,結(jié)果婚禮上潜沦,老公的妹妹穿的比我還像新娘萄涯。我一直安慰自己,他們只是感情好唆鸡,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布涝影。 她就那樣靜靜地躺著,像睡著了一般争占。 火紅的嫁衣襯著肌膚如雪燃逻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天臂痕,我揣著相機與錄音伯襟,去河邊找鬼。 笑死刻蟹,一個胖子當著我的面吹牛逗旁,可吹牛的內(nèi)容都是我干的嘿辟。 我是一名探鬼主播舆瘪,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼红伦!你這毒婦竟也來了英古?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤昙读,失蹤者是張志新(化名)和其女友劉穎召调,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡唠叛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年只嚣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艺沼。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡册舞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出障般,到底是詐尸還是另有隱情调鲸,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布挽荡,位于F島的核電站藐石,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏定拟。R本人自食惡果不足惜于微,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望办素。 院中可真熱鬧角雷,春花似錦、人聲如沸性穿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽需曾。三九已至吗坚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呆万,已是汗流浹背商源。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谋减,地道東北人牡彻。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像出爹,于是被迫代替她去往敵國和親庄吼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法严就,類相關(guān)的語法总寻,內(nèi)部類的語法,繼承相關(guān)的語法梢为,異常的語法渐行,線程的語...
    子非魚_t_閱讀 31,639評論 18 399
  • 一祟印、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,265評論 0 16
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等肴沫,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,501評論 0 3
  • 炸了毛的喵閱讀 99評論 0 0
  • 我是一粒小小的塵埃 我愿隨風飄蕩自由自在 我愿沒有目的地流浪 風停,塵落 風起蕴忆,塵揚 不愿為誰駐足停留 不愿有所牽...
    米粒的世界閱讀 207評論 1 1