1. 程式人生 > >Java集合類:ArrayList

Java集合類:ArrayList

前言

今天學習一個Java集合類使用最多的類 ArrayList , ArrayList 繼承了 AbstractList,並實現了ListRandomAccess 等介面,

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

是一個以陣列形式儲存資料的集合,它具有以下的特點:

集合中的陣列是有序排列的;

允許元素為null;

允許重複的資料;

非執行緒安全;

針對它的這些特點,我們一步步跟進原始碼進行解析。

原始碼解析

基本成員變數

先看下ArrayList 的基本成員變數

transient Object[] elementData;
private static final int DEFAULT_CAPACITY = 10;
private int size;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

elementData :一個object陣列,組成了ArrayList 的底層資料結構,由於陣列型別為 Object,所以允許新增 null 。值得注意的是,變數的修飾符是 transient

,這說明這個陣列無法被序列化,但是之前ArrayList卻實現了序列化介面,是可以被初始化,這不是互相矛盾了嗎,關於這個原因,下面會說到,別急。

DEFAULT_CAPACITY : 陣列的初識容量

size : 陣列元素個數

MAX_ARRAY_SIZE: 陣列最大容量

說完變數後,開始學習ArrayList 的基本方法,我們都知道,一個集合最重要的方法就是對元素的 增刪改查操作,瞭解了這些操作的方法,也就基本瞭解了容器的運作機制。

新增元素

ArrayList 中最基礎的新增元素方法是 add(E e)

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

原始碼邏輯比較簡單,主要是先做了調整容量處理,並把元素存入陣列的最後一個位置。

來看一下調整容量的方法 ensureCapacityInternal(),原始碼如下:

private void ensureCapacityInternal(int minCapacity) {
	//如果是空陣列,初始化容量,取預設容量和 當前元素個數 最大值
    if (elementData == DEFAULTCAPACITY_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;
        //新容量為舊容量的1.5倍
        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);
    }

可以看到,調整容量的過程其實是擴容了陣列的容量,並在最後呼叫了Arrays的copyOf方法 ,等於把元素組裡面的內容複製到新的數組裡面去,

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    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;
}

用一張圖來表示是這樣的 在這裡插入圖片描述

除了上面的add 方法外,ArrayList還提供了幾個新增操作的方法,分別是

//在指定位置新增元素
public void add(int index, E element) {
	//判斷index是否在size範圍內
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //整體複製,並後移一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
//新增整個集合
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
 }
 
 //在指定位置,新增一個集合
 public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
	    //把該集合轉為物件陣列
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

這三個方法雖然也是插入的操作,但其實最後都是呼叫 System 的 arraycopy 方法做一個整體的複製,這是一個native的方法,功能就是把陣列做一個整體的複製,並向後移了一位。如果要複製的元素很多,那麼就比較耗費效能,這是比較不好的一點。

所以,一般情況下,ArrayList適合順序新增的情景。

查詢元素

E elementData(int index) {
        return (E) elementData[index];
}

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

    return elementData(index);
}

查詢的程式碼比較簡單,都是直接返回陣列對應位置的元素,充分利用了陣列根據索引查詢元素的優勢嗎,因此效率較高。

擴充套件:說到查詢,來提一個知識點,那就是遍歷元素的效率問題,前面說了,ArrayList 實現了RandomAccess 介面,所以 遍歷ArrayList 的元素 get() 獲取元素在效率上是優於迭代器的,至於原因,在 《Java集合類:“隨機訪問” 的RandomAccess介面》有介紹,這裡不進行敘述了。

修改元素

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

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

也是根據索引運算元組 ,不多說。

刪除元素

ArrayList刪除元素的方法比較多,但說起來無非是三類,

1、按照下標刪除元素

2、按照元素刪除,這會刪除ArrayList中與指定要刪除的元素匹配的第一個元素

3、清除陣列元素

前面兩種雖然功能不同,但程式碼的最終呼叫是差不多的,都是引用類似這段程式碼來解決問題:

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

這段程式碼的邏輯大概就是把指定元素後面的所有元素整體複製並向前移動一個位置,然後最後一個元素置為null,跟插入中的部分方法邏輯很像,都是呼叫 System.arraycopy 來做陣列操作,所以說,刪除元素的效率其實也是不高的。

而第三類刪除的方法其實是呼叫 clear

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

把陣列的每個元素都置為null,並把size置為0,就這麼簡單。

為什麼用 “transient” 修飾陣列變數

最後一個問題,之前說了ArrayList 可以被序列化,但其陣列變數卻是用transient 修飾,這是為什麼呢?按照 五月的倉頡 大神這篇文章的解釋就是:

序列化ArrayList的時候,ArrayList裡面的elementData未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那麼是否有必要序列化整個elementData呢?顯然沒有這個必要,因此ArrayList中重寫了writeObject方法:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

每次序列化的時候呼叫這個方法,先呼叫defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然後遍歷elementData,只序列化那些有的元素,這樣:

1、加快了序列化的速度

2、減小了序列化之後的檔案大小

不得不說,設計者還是很用心良苦的。

總結

ArrayList 的原始碼分析就到這裡了,因為其底層只有陣列,所以原始碼的邏輯還是比較簡單,比起HashMap這樣的學習起來要輕鬆多了 (HashMap原始碼學習過程回想起來真是痛苦啊?) ,下面總結一下它的一些知識點:

1、ArrayList 的插入刪除操作比較慢,因為涉及到整個陣列的複製和移動操作。

2、因為是陣列,支援隨機訪問,所以查詢和修改的速度較快

3、ArrayList 的方法裡沒有做同步操作,所以ArrayList 是非執行緒安全的,一般使用的時候可以呼叫 Collections.synchronizedList 來包裝。

感謝大神的文章: