原始碼閱讀之ArrayList實現細節

hylinux1024發表於2018-11-06

0x00 描述

ArrayList 可以說是 Java 程式猿最為常用的一種資料結構了。ArrayList 是通過陣列實現的,容量可以自增的線性表。而陣列的優點是計算機可以通過下標計算訪問地址,所以訪問元素的速度是很快的,時間複雜度為O(1);但陣列並不擅長插入和刪除操作,這些操作的時間複雜度是O(n)。因此 ArrayList 繼承了陣列這些特點。

繼承關係

ArrayList 繼承於 AbstractList 並實現了 ListRandomAccessCloneableSerializable 介面。

AbstractList 是繼承於 Collection 介面。因此簡單的關係圖可以表達為

array-structure

重要屬性
  • elementData 這個是存放資料的 Object 陣列
  • size 記錄當前陣列元素的個數
  • modCount 用於記錄修改次數,例如增加、刪除等操作時此變數會自增。當這個變數異常變化時,會丟擲 ConcurrentModificationException
構造方法
public ArrayList() {
	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製程式碼

預設建構函式初始化 elementData 大小為10 空陣列。

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);
    }
}
複製程式碼

此方法通過一個 initialCapacity 變數對陣列進行初始化。當傳入的 initialCapacity 大於0時,elementData就是初始化為大小為 initialCapacity 的空陣列;否則就是初始化為大小為0的空陣列。

public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();
   	if ((size = elementData.length) != 0) {
		// c.toArray might (incorrectly) not return Object[] (see 6260652)
		if (elementData.getClass() != Object[].class)
			elementData = Arrays.copyOf(elementData, size, Object[].class);
	} else {
		// replace with empty array.
		this.elementData = EMPTY_ELEMENTDATA;
	}
}
複製程式碼

這個方法是通過一個 Collection 物件進行初始化的。這裡呼叫了 Arrays.copyOf 方法將陣列元素進行拷貝,並返回一個新的陣列。後文會詳細解析這個方法。

0x01 常用方法

add(E e)

ArrayList 新增一個元素

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

新增元素之前,呼叫了 ensureCapacityInternal 方法,確保 elementData 陣列有足夠的空間。然後陣列後面新增一個元素,並把元素個數 size 的值加1。

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

	ensureExplicitCapacity(minCapacity);
}
複製程式碼

ensureCapacityInternal 中先判斷 elementData 是否為空陣列,如果是,則取 DEFAULT_CAPACITYminCapacity 的最大值作為陣列的最小容量。

然後再執行 ensureExplicitCapacity 方法。

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

	// overflow-conscious code
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}
複製程式碼

先把 modCount 加1,表示對該列表進行了一次操作。

minCapacity 表示目前需要的容量大小。如果它大於目前 elementData 的容量大小,那麼就會執行 grow 方法增加陣列容量。

private void grow(int minCapacity) {
	// overflow-conscious code
	int oldCapacity = elementData.length;
	int newCapacity = oldCapacity + (oldCapacity >> 1);//右移1位操作相當於除2
	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. 先獲取到 newCapacity ,它是原來容量大小的1.5倍
  2. 如果需要的容量大小 minCapacity 大於原來容量的1.5,那麼 newCapacity 就取 minCapacity
  3. 如果 newCapacity 還大於最大的容量,那麼就執行 hugeCapacity 來計算得到容量大小
  4. 最後呼叫 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;
}
複製程式碼

因此在呼叫 add 方法時,如果當前陣列 elementData 的容量不夠時,就會呼叫擴容的 grow 方法,把陣列擴大為原來的1.5倍的大小。

add(int index, E element)

在指定的 index 位置上新增一個元素

public void add(int index, E element) {
	if (index > size || index < 0)
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

通過上面的 add 方法的走讀,這個方法就很好理解了。

先對 index 引數的有效性進行判斷;

然後執行 ensureCapacityInternal 確保陣列的容量大小是足夠的,此時 modCount 也會自增;

再執行 System.arraycopy 方法把陣列元素從 index 的位置後移1位;(System.arraycopy 函式後文還會講到)

最後在 index 位置上賦值,並把 size 加 1。

addAll(Collection<? extends E> c)
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;
}
複製程式碼

addAll 方法把一個 Collection 物件新增到列表中來。

它會先把 Collection 物件通過 toArray 方法轉化為陣列,然後再呼叫 System.arraycopy 進行資料的移動。

addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
	if (index > size || index < 0)
		throw new IndexOutOfBoundsException(outOfBoundsMsg(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;
}
複製程式碼

index 位置上新增一個列表

它與上面 addAll 方法的區別就是先從 index 開始移動 numNew 個位置,即空出 numNew 個位置。

然後再空出的 numNew 位置上新增元素。

remove(int index)

刪除指定 index 位置上的元素

public E remove(int index) {
	if (index >= size)
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

	modCount++;
	E oldValue = (E) 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;
}
複製程式碼

執行流程為

  1. 檢查 index 有效性,無效則丟擲 IndexOutOfBoundsException 異常
  2. modCount 自增
  3. 通過 index 下標取出元素
  4. 計算 index 後面需要移動的元素個數
  5. 通過 System.arraycopyindex 後面的元素都往前移動1位
  6. 最後把末尾元素置位 null,並把 size 的值減 1。
remove(Object o)

通過一個元素物件進行刪除

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;
}
複製程式碼

當傳一個元素物件進行刪除操作時,需要遍歷陣列,找到該元素在列表中的位置 index

然後通過 fastRemove 方法進行刪除。

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
}
複製程式碼
clear()

清空列表

public void clear() {
	modCount++;

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

	size = 0;
}
複製程式碼
subList(int fromIndex, int toIndex)

獲取子列表

public List<E> subList(int fromIndex, int toIndex) {
	subListRangeCheck(fromIndex, toIndex, size);
	return new SubList(this, 0, fromIndex, toIndex);
}

static void subListRangeCheck(int fromIndex, int toIndex, int size) {
	if (fromIndex < 0)
		throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
	if (toIndex > size)
		throw new IndexOutOfBoundsException("toIndex = " + toIndex);
	if (fromIndex > toIndex)
		throw new IllegalArgumentException("fromIndex(" + fromIndex +") > toIndex(" + toIndex + ")");
}
複製程式碼

首先檢查下標是否正確,然後構造一個 SubList 物件,這是一個內部類。

SubList 也是繼承於 AbstractList

private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;

        SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }

        public E set(int index, E e) {
            if (index < 0 || index >= this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            E oldValue = (E) ArrayList.this.elementData[offset + index];
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }

        public E get(int index) {
            if (index < 0 || index >= this.size)
              throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            return (E) ArrayList.this.elementData[offset + index];
        }

        ...

        public void add(int index, E e) {
            if (index < 0 || index > this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

        public E remove(int index) {
            if (index < 0 || index >= this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }

        ...

        public boolean addAll(Collection<? extends E> c) {
            return addAll(this.size, c);
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            if (index < 0 || index > this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            int cSize = c.size();
            if (cSize==0)
                return false;

            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            parent.addAll(parentOffset + index, c);
            this.modCount = parent.modCount;
            this.size += cSize;
            return true;
        }
    
        ...
    }
複製程式碼

SubList 構造方法需要一個父列表。在獲取、新增、刪除元素的方法中實際上都是呼叫父列表中的方法。

不過這些操作的方法中會判斷 modCount 的值是否已經變化,如果異常改變了,那麼就會丟擲 ConcurrentModificationException 異常。

forEach(Consumer<? super E> action)

遍歷列表元素

public void forEach(Consumer<? super E> action) {
	Objects.requireNonNull(action);
	final int expectedModCount = modCount;
	@SuppressWarnings("unchecked")
	final E[] elementData = (E[]) this.elementData;
	final int size = this.size;
	for (int i=0; modCount == expectedModCount && i < size; i++) {
		action.accept(elementData[i]);
	}
	// Android-note:
	// Iterator will not throw a CME if we add something while iterating over the *last* element
	// forEach will throw a CME in this case.
	if (modCount != expectedModCount) {
		throw new ConcurrentModificationException();
	}
}
複製程式碼

同樣地,此方法中如果 modCount 被異常修改了(例如在其他執行緒中執行了 add 方法)那麼就會丟擲 ConcurrentModificationException 異常。

iterator()

獲取遍歷器

public Iterator<E> iterator() {
	return new Itr();
}
複製程式碼

Itr 是一個內部類,實現了 Iterator 介面。

private class Itr implements Iterator<E> {
	// Android-changed: Add "limit" field to detect end of iteration.
	// The "limit" of this iterator. This is the size of the list at the time the
	// iterator was created. Adding & removing elements will invalidate the iteration
	// anyway (and cause next() to throw) so saving this value will guarantee that the
	// value of hasNext() remains stable and won't flap between true and false when elements
	// are added and removed from the list.
	protected int limit = ArrayList.this.size;

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

	@SuppressWarnings("unchecked")
	public E next() {
		if (modCount != expectedModCount)
			throw new ConcurrentModificationException();
		int i = cursor;
		if (i >= limit)
			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();
		if (modCount != expectedModCount)
			throw new ConcurrentModificationException();

		try {
			ArrayList.this.remove(lastRet);
			cursor = lastRet;
			lastRet = -1;
			expectedModCount = modCount;
			limit--;
		} catch (IndexOutOfBoundsException ex) {
			throw new ConcurrentModificationException();
		}
	}

	...
}
複製程式碼

該類中 cursor 屬性記錄了當前迭代的位置,每呼叫一次 next 方法都會加 1,lastRet 則記錄了上一次的元素位置。

remove 方法則是通過呼叫外部類的 remove 方法來實現的。

以上兩個方法中也需要注意 ConcurrentModificationException 異常的發生。

contains(Object o)

檢測是否包含元素

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;
}
複製程式碼

可以看出要檢測一個元素是否在列表中,是通過遍歷來實現的。

System.arraycopy
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,int length);
複製程式碼

這是陣列拷貝函式,是 native 函式,它經過虛擬機器優化的,效率比較高。在 ArrayList 中移動元素就是通過這個方法。

Arrays.copyOf
public static <T> T[] copyOf(T[] original, int newLength) {
	return (T[]) copyOf(original, newLength, original.getClass());
}

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;
}
複製程式碼

可以看出 copyOf 函式最終呼叫的是 System.arraycopy 方法。本文中 grow 方法就是呼叫 copyOf 來實現擴容的。

0x02 總結

  • ArrayList 是基於陣列實現的線性表,它支援自動擴容,每次增加原來容量的1.5倍。
  • 通過下標獲取元素操作效率高,而刪除和插入操作則需要移動元素,效率不高。
  • remove 函式通過物件刪除元素時需要遍歷列表,而通過下標 index 刪除元素比通過物件刪除元素的效率要高。
  • containtsclear方法需要遍歷。
  • subList 獲取到子列表,對子列表的修改同樣也會修改父列表。
  • ArrayList 沒有同步鎖,在多執行緒操作時需要注意 ConcurrentModificationException 異常。
  • 如果在同一執行緒中對 ArrayList 操作時引起 modCount 異常改變時,也要注意 ConcurrentModificationException ,這時候要檢查程式碼邏輯問題。

相關文章