從源碼閱讀認識ArrayList

優(yōu)秀源碼的閱讀對于開發(fā)者來說是一個不錯的提升方法。今天我們就要通過JDK源碼的閱讀來重新認識大家所熟悉的ArrayList

ArrayList
//源碼
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private transient Object[] elementData;
private int size;

ArrayList定義了這四個參數(shù):
DEFAULT_CAPACITY是默認的容量
EMPTY_ELEMENTDATA定義了一個空的數(shù)組
elementData是列表中存放對象的數(shù)組
size該列表的所存放的對象數(shù)量

擴展閱讀——transient
細心的讀者會發(fā)現(xiàn)elementData被關(guān)鍵字transient修飾,這意味著elementData不參與序列化。
這么設(shè)計的原因是由于ArrayList的自動擴容機制,保證了ArrayList的容量總是大于等于其所
存放的對象,如果對elementData進行序列化就會造成浪費。因此ArrayList的序列化是對于
elementData所存放的對象進行,而不是elementData本身
部分源碼:
for (int i = 0; i < size; i++){ //遍歷上限為size,而非elementData.length
    s.writeObject(elementData[i]);
}
可以看到,JDK底層的源碼對于內(nèi)存空間的使用是非常謹慎的。優(yōu)秀的源碼如是,優(yōu)秀的應(yīng)用
也應(yīng)如是。

ArrayList的底層是基于數(shù)組實現(xiàn)的。既然ArrayList是用數(shù)組對象來作為存儲容器,那么其內(nèi)存模型也和數(shù)組是一致的,是一個連續(xù)的大小相同的內(nèi)存。

List<String> list = new ArrayList<String>(5);
list.add("Hello");
list.add("world");
lsit.add("!");
ArrayList內(nèi)存模型

如圖所示,在這段源碼中數(shù)組對象的長度為5,但是調(diào)用list.size()則返回的是3。

構(gòu)造方法
//源碼
    //構(gòu)造方法一
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

    //構(gòu)造方法二
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    //構(gòu)造方法三
    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);
    }

ArrayList有三種構(gòu)造方法(見源碼),其中構(gòu)造方法一是最常用的,這里elementData引用了空的對象數(shù)組,當需要容量時,會執(zhí)行擴容操作(詳細見add方法);構(gòu)造方法二讓ArrayList在初始化的時候遍分配好了所需要的內(nèi)存容量,筆者建議使用ArrayList的時候,如果能夠知道即將添加入數(shù)組列表中的數(shù)據(jù)量,則選擇這個構(gòu)造方法預(yù)先申請好內(nèi)存優(yōu)于其自動擴容,畢竟擴容操作是有開銷的;構(gòu)造方法三可以將已有的Collection子類直接轉(zhuǎn)成ArrayList類型數(shù)據(jù)。

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

add()比較簡單,首先確認下容量是否足夠(不夠就進行擴容),然后將待添加的對象放置于數(shù)組當中。
ensureCapacityInternal()方法就是ArrayList比一般的數(shù)組靈活的核心所在。大家都清楚,ArrayList的動態(tài)擴容避免了數(shù)組越界的問題。接下來就看看ArrayList是如何實現(xiàn)動態(tài)擴容機制。

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

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

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

grow()方法就是對ArrayList進行擴容的操作,判斷是否擴容的條件就是minCapacity - elementData.length>0,當ArrayList所需的最小容量大于elementData數(shù)組的容量。
oldCapacity >> 1移位操作的細節(jié),相比于直接除以2,得到的結(jié)果是一樣的,但是由于計算機底層實現(xiàn)是通過移位來實現(xiàn)的,所以直接調(diào)用移位方法比直接調(diào)用'/'的數(shù)學(xué)運算運算速度來得快。優(yōu)秀的源碼真是讓人受益匪淺,細心點,可以發(fā)現(xiàn)不少可以幫助你優(yōu)化程序性能的地方。

ArrayList動態(tài)擴容

通過這段源碼,可以知道,擴容機制,就是將數(shù)組中的內(nèi)容復(fù)制到另一個容量更大的數(shù)組中。這也是為什么筆者建議盡可能地指定ArrayList的初始容量,特別是在容量需求較大是,不指定初始容量會讓ArrayList不斷擴容,不斷地讀寫內(nèi)存,導(dǎo)致效率更低。

擴展閱讀——機制的利用與濫用
可能有很多讀者覺得ArrayList提供了動態(tài)擴容的機制,就無需注意這些使用上
的細節(jié),這個想法是錯誤的,Jvm給我們提供了垃圾回收機制,但很多源碼還是
會及時地對數(shù)據(jù)進行回收處理,來獲得更為強大的性能。詳情可以去了解Java GC
相關(guān)知識。
小實驗:
ArrayList<String> list = new ArrayList<String>();  //耗時0ms
for(int i = 0; i<1000000; i++){  //耗時15ms
            list.add("hello");
}
ArrayList<String> list2 = new ArrayList<String>(1000000);  //耗時1ms
for(int i = 0; i<1000000; i++){  //耗時4ms
            list2.add("hello");
}
get()
//源碼 get的實現(xiàn)
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

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

經(jīng)??丛创a,可以讓你的代碼變得優(yōu)雅,很多人覺得,這里rangeCheck()方法只有一個判斷,為何還要單獨一個方法,如果去細看其他方法的實現(xiàn),會發(fā)現(xiàn)這個方法被多次的調(diào)用。本人在項目上見得最多的一個反例,就是這樣的代碼:

反例:
Object  object = objectDao.get(objectId);  //Object代指某個實例
if(object == null){
  throw new xxxException("object不存在")
}

推薦做法:
public Object getObjectEntity(String objectId){
    Object  object = objectDao.get(objectId);  //Object代指某個實例
    if(object == null){
        throw new xxxException("object不存在")
    }
    return object;
}

但凡需要獲取這個Object對象,都是這樣的一段代碼。而且經(jīng)常會因為忘記條件語句而出現(xiàn)NPE(空指針異常)。用本人的話來說,就是“不夠面向?qū)ο蟆?。雖然很簡單,不就是封裝嗎,但是很多開發(fā)者都做不到,導(dǎo)致大量不易看懂的業(yè)務(wù)邏輯,大量的重復(fù)代碼。

remove()
//源碼 remove的實現(xiàn)
    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
    }

簡單提一下,對于不需存活的對象,比如被Listremove的對象,可以將對對象的引用指向null,這樣GC就會回收這個對象,避免造成內(nèi)存的浪費,對于提升程序的性能是很有幫助的。

對于ArrayList的其他方法就不多說了,有興趣的建議去看看源碼實現(xiàn)。
歡迎查看下一篇相關(guān)的文章《從源碼閱讀認識LinkedList》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容