讀 Java 11 源碼(1)ArrayList

主要變量一覽

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;

上面這些參數的含義基本上,大家都曉得了,這裏還是大概的說一下:

  • DEFAULT_CAPACITY:默認的初始化的數組的大小,爲10
  • EMPTY_ELEMENTDATA:空數組,存儲的數組沒有被複制,那麼就會調用這個。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:和EMPTY_ELEMENTDATA區別開。主要是使用的時候擴容的策略不同。這裏我們先跳過,後面單獨說擴容的策略到時候就會說到了。
  • elementData:這個最重要了,存儲的數據基本數組就是這個了。
  • size:當前存在數組中有多少的元素,我們一般都是看這個。

爲什麼存儲數據的數組 elementData 要用 transient來修飾?
我像是爲了節約空間,因爲我們的數組在大部分的情況下是不會被填滿的,一般一滿就擴容,所以如果在序列化的時候,直接就用elementData來擴容,本質上是浪費空間的。我們從下面的代碼也可以看出來:

/**
 * Reconstitutes the {@code ArrayList} instance from a stream (that is,
 * deserializes it).
 * @param s the stream
 * @throws ClassNotFoundException if the class of a serialized object
 *         could not be found
 * @throws java.io.IOException if an I/O error occurs
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // like clone(), allocate array based upon size not capacity
        SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
        Object[] elements = new Object[size];// 壹

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

        elementData = elements;
    } else if (size == 0) {
        elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new java.io.InvalidObjectException("Invalid size: " + size);
    }
}

在壹中,這個是按照size的大小來從新塑造了一個數組,然後進行序列化,其實是節約了空間的。

查找

這個最簡單,基本邏輯上就是遍歷,然後匹配,但是在匹配上,有講究。

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

我們要確定是這查找的值是否爲null,如果爲null,就 == 來比較,這個一般比較的是引用;而如果不是null,那麼走的就是equals的邏輯,一般來說,我們常用的String,Integer(這種基本類型的包裝類)都是有重寫equals的邏輯的,不是比較引用,而是比較值。

增加

這段是主要的add的代碼邏輯,首先我們要知道在 數組 的某個位置上增加一個元素的邏輯就是,首先把那個元素之後的所有元素給搬運到後一個位置,然後 把index位置上的元素賦值給需要被增加的元素。

public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    // 當前的存儲數據的數組是否已滿
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    // 搬運元素
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    elementData[index] = element;
    size = s + 1;
}
private void rangeCheckForAdd(int index) {
	  if (index > size || index < 0)
	       throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

其實這段代碼就是告訴我們,在index的位置上插入一個元素,主要是就是先確認傳入的index是否是合理的,如果合理,再判斷,當前的存儲的數據的數組是不是已經滿了,滿了就擴容,然後複製元素到新的數組,然在新的數組裏再插入。

擴容操作

我們一般情況下是不會在使用開始的就確切的知道這次數據量的大小的,所以當存儲數據的數據結構滿的時候,就要進行擴容。

private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));
}

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個參數就在這裏發生了作用了,當然,這個我們還要結合構造函數來看看,構造函數有三個,但是因爲第三個用集合來構造我們不用考慮,所以我們來看看以下兩個構造函數代碼:

 /**
* Constructs an empty list with the specified initial capacity.
 *
 */
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);
    }
}

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

如果是無參構造,那麼elementData就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
如果有參構造並且initialCapacity=0,那麼elementData就是EMPTY_ELEMENTDATA。

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
	return Math.max(DEFAULT_CAPACITY, minCapacity);

擴容的時候,如果elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
就從DEFAULT_CAPACITY和參數minCapacity之中選個最大的值開始擴,也就是這次擴容,最小也是DEFAULT_CAPACITY。
打個比方,如果我們直接無參初始化:

ArrayList<Integer> list = new ArrayList<>();

那麼我們發生第一次add操作的時候要進行grow,grow(擴容)出來的數組大小就是10了;我們再進行add操作,就不用grow(擴容)操作了。
但是如果我們進行有參初始化;

ArrayList<Integer> list = new ArrayList<>(0);

那麼我們第一次add操作的時候,也要進行grow,grow(擴容)出來的數組此時爲1;我們再進行add操作,還要grow(擴容)操作。
why? 其實這裏我猜測是這樣的:爲了優化空間。我們用指定大小的初始化對象的方式就是爲了提升效率,節約空間。如果我們壓根沒有10的元素要放到list中,那麼默認的初始化就無形之中浪費了空間,這很不極客。所以,我猜測這種區別無參和帶參數的擴容機制是爲了節省空間的。

以上就可以看出,其實這裏代表的有參初始化無參初始化的擴容策略的不同。

刪除操作

刪除index位置上元素的基本邏輯就是:把index位置後面的元素全都往前挪動一個位置,這樣就可以把index位置上的值覆蓋,這就是刪除了。

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 
 */
public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);

    return oldValue;
}
/**
 * Private remove method that skips bounds checking and does not
 * 主要的刪除的邏輯就是這一段
 */
private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

以上的刪除的邏輯就是把,通過System.arraycopy(es, i + 1, es, i, newSize - i); ,把後一個給搬到前一個,然後,最後那個賦值爲null主要是方便JVM的垃圾回收器的垃圾回收。

內置迭代器

理解這還是蠻重要的,因爲涉及一些和集合類的使用的規範的問題。

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;
    // prevent creating a synthetic constructor
    Itr() {}
    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();
    }
}

阿里的規範中明確的提到,不要在foreach的操作中,修改數據結構,這裏指的是刪除或者增加操作,我們這裏舉個例子

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        for (Integer item : list) {
            list.remove(item);
            System.out.println(item);
        }
    }

編譯後,再反編譯後的代碼,編譯工具jad

public static void main(String args[])
{
    ArrayList arraylist = new ArrayList(Arrays.asList(new Integer[] {
        Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5)
    }));
    Integer integer;
    for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(integer))
    {
        integer = (Integer)iterator.next();
        arraylist.remove(integer);// (1)
    }

}

這foreach用的是,會被編譯成迭代器,用的還是ArrayList的內置的迭代器。但是問題出現在(1)的位置,這裏用的remove的方法,這方法會改變modcount,但是不會改變expectedModCount,這就會在

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

這段代碼裏報錯,這裏的報錯主要是爲了實現一個叫做fail-fast機制。
之所以有這個機制,主要是爲了防止出現兩個線程,一個迭代,一個修改的情況。這種情況是不允許的,比如迭代的想找某個程序,結果被修改的給刪了,倒是接下來的步驟出錯。
所以,你如果想要實現刪除,那麼就不要用foreach的方式來刪除,還是老老實實的用迭代器iterator.remove()來刪除吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章