1. 程式人生 > >java 集合類之ArrayList

java 集合類之ArrayList

本文將從原始碼的角度對Java 最常用的集合類ArrayList進行介紹,程式碼版本為1.8_121。

繼承結構

ArrayList繼承結構
除了一些功能性的介面,ArrayList的繼承大致可以看成是從Collection=>AbstractCollection=>AbstractList=>ArrayList
的一個繼承流程,接下來依此看看這些類都做了什麼工作。

Collection類

collection 類提供了三類介面:

  1. 查詢介面 size() ,isEmpty,contains(),iterator,toArray
  2. 修改介面 add(), remove()
  3. 批量操作介面 containsAll(),addAll(),removeAll(),removeIf(),retainAll(),clear()
    可以著重提一下的是boolean retainAll(Collection<?> c);
    這個方法做的是一個交集操作,也就是隻有引數裡面有的才會被保留,返回是否做了修改。
    另外還有一個removeIf()可以傳入一個判定函式,類似於filter的作用,但是不用先轉化為stream,不過需要注意的是這個方法不是執行緒安全的,

Collection類在1.8 之後針對平行計算做了一些支援,加入了幾個方法,在這裡順便簡單介紹一下:

  1. spliterator(),這個方法類似於iterator(),但是他返回的是一個分割迭代器,這個迭代器的特點是它有一個trySplit()函式,這個函式可以將這個分支迭代器進行分支,得到將流劃分為多個分支的效果,以在多個核心上並行執行的效果。例如一個最簡單的實現就是將剩下的還沒遍歷的元素劃分為相等的兩部分,將序號較小的一部分封裝到一個Spliterator裡返回,然後將當前Spliterator的index更新為中值加1
  2. Stream<E> stream() 是函數語言程式設計的支援,類似於使用Iterator遍歷,但是可以對stream進行函數語言程式設計的操作,如使用map、filter函式來處理Stream
  3. Stream<E> parallelStream(),返回一個並行的stream

重要的Field

  1. transient Object[] elementData; 儲存資料的物件陣列,從這裡可以看出Java泛型底層使用的是型別擦除,直接使用了Object的陣列儲存資料
  2. private int size;陣列的長度

重要方法實現

  1. add(E),插入元素
	 /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

非常簡單的將一個元素放到陣列中,但是需要注意的是這裡之前的ensureCapacityInternal()
方法。這個方法的呼叫主要是做了兩件事,第一是修改modCount,以便在Iteration裡面能檢測ConcurrentModification然後丟擲異常,另外一個就是當長度不夠時修改長度

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
    }

可以看到每次的容量是原來的1.5倍,或者直接增長到所需的大小或者是最大的大小,然後使用Arrays.copyOf()將資料拷貝到一個新的陣列中去,所以在這裡可以看到的是如果能夠預知陣列的大小,那麼在一開始就設定大小會比較好,就能省去在增長容量時資料複製的時間。

  1. remove
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;
    }

在刪除之後使用了arraycopy()方法來移動列表,需要指出的是,arraycopy函式在處理src和dest相同的複製時會首先將需要複製的內容儲存到一個臨時的表裡,然後再移動,所以在對一個容量超大的列表進行刪除操作的時候,可以從這個角度尋找優化的空間。
為了印證刪除過程中移動的時間開銷,寫了一個小測試程式,分別從頭和從尾部逐個刪除一個數組:

public void testArrayList() {
        try {
            int length = 100000;
            ArrayList<Integer> list = new ArrayList<>(length);
            for (int i = 0; i < length; i++) {
                list.add(i);
            }
            ArrayList<Integer> list2 = new ArrayList<>(length);
            for (int i = 0; i < length; i++) {
                list2.add(i);
            }
            long begin = System.currentTimeMillis();
            for (int i = 0; i < length; i++) {
                list.remove(0);
            }
            System.out.println(System.currentTimeMillis()-begin);
            begin = System.currentTimeMillis();
            for (int i = length-1; i >=0; i--) {
                list2.remove(i);
            }
            System.out.println(System.currentTimeMillis()-begin);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

輸出為:
1098
6
可以看出在一個不是很大(100,000)的列表裡分別從兩頭刪除的效能差距已經到了150倍以上