Java ArrayList 源代碼分析


Java ArrayList

之前曾經參考 數據結構與算法這本書寫過ArrayList的demo,本來以為實現起來都差不多,今天抽空看了下jdk中的ArrayList的實現,差距還是很大啊

首先看一下ArrayList的類圖

ArrayList實現了Serializable Cloneable RandomAccess List這幾個接口,可序列化,可克隆,可以隨機訪問

構造方法:

public ArrayList() {
	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

之前手寫ArrayList的時候,都會用一個默認容量來 new 一個數組,在jdk中實現是默認一個空數組,因為有的時候ArrayList創建后並不會添加元素

當然,這兩個都是靜態私有域

值得注意的是 this.elementData

是一個Object的數組 transient表示這個屬性不用被序列化,通過注釋可以得知,element在第一次添加的時候會被擴容到默認容量(默認為10)

add 方法

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

add 方法中調用了 ensureCapacityInternal相當於確保容量最少是size+1,size就是當前ArrayList元素個數,然后在elementData末尾加入元素

接下來看一下是如何確保容量的

private static int calculateCapacity(Object[] elementData, int minCapacity) {
	if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
		return Math.max(DEFAULT_CAPACITY, minCapacity);
	}
	return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {
	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

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

	// overflow-conscious code
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

ensureCapacityInternal首先會調用calculateCapacity,這里主要是為了計算第一次初始化的時候,因為我們在默認初始化的時候,默認容量是10,但是為什么確保闊容是Math.max(DEFAULT_CAPACITY, minCapacity);,這里主要是因為如果我們添加一個集合的話,要確保至少大小是集合中元素的大小,否則可能會多一次擴容

然后調用ensureExplicitCapacity

ensureExplicitCapacity:先設置一下當前容器已經被更改,然后判斷當前最少需要容量是不是大於數組長度,如果大於,那就擴容

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

首先獲取舊數組的長度然后用舊數組長度進行擴容為1.5倍,然后判斷和最小需求容量對比,如果小於最小容量,那么就擴容到最小容量那么長,然后判斷是不是大於一個閾值,如果大於這個最大閾值,那么就擴容到Integer.MAX_VALUE(正整數最大值,2^31-1)

至於為什么要判斷minCapacity<0,那是因為假設當前已經擴容到最大值,要是還不夠,那么再擴容就是int溢出

最后把源數組copy到新的容量大小賦值給elementData,Array.copyOf底層是native方法(System.arraycopy)

之前自己寫的ArrayList都是通過 oldcaptain = oldcaptain<<1+1;來進行擴容的(+1是避免舊數組長度為0的情況),jdk對於不同的情況有不同的擴容標准,而且以前自己的Copy都是用數組遍歷Copy的很笨重,這里學到了

再來看一下 add(int index,T ele)

public void add(int index, E element) {
	rangeCheckForAdd(index);

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

這個也很好理解,就是先檢查index是否在范圍內(0~size)如果不在就拋出一個越界異常

然后准備擴容,接下來就是數組拷貝

System.arraycopy也是一個native方法

看一下注釋就是把srcsrcPos開始拷貝到destdestPos開始的位置一共copy length這么長

如果src==dst那么這個函數表現就像先拷貝到一個臨時數組,再覆蓋dst對應位置

不會像*dst++=*src++把后面的元素覆蓋然后后面元素都是一個值

這樣就是把elementData從index開始到最后一個元素,拷貝到src+1的位置

最后執行elementData[index] = element;把元素覆蓋

然后我們看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;
}

remove方法跟add基本同理,但是不需要擴容而且最后覆蓋元素的時候是使用null填充最后一個元素

之前實現的時候沒考慮到用null覆蓋,這樣會導致在GC的時候,本來需要刪除的元素還可以通過ArrayList找到,然后就無法GC,這里學到了

remove一個對象

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

找對應元素的話基本都是大同小異,主要是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
}

fastRemove里面跟remove基本相同,少了一個index判斷也沒有返回值

clear:

public void clear() {
    modCount++;

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

    size = 0;
}

clear方法之前一直以為是直接把size設為0,但是jdk里面實現是遍歷一下設null,但是這里我總覺得應該再多提供一個fastclear什么的比較好吧

設為null會讓對象索引不到,可以被垃圾回收,但是如果頻繁add clear的話總覺得不值得啊

再看一下一些跟集合的操作

通過一個集合初始化:

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

這里首先調用集合的toArray()方法,不過要確保elementData真的是一個Object[]數組

Java 中對象數組子類數組引用也可以轉換為超類的引用

比方說 Manager 繼承了 Employee

Manager[]managers = new Manager[10];

那么我們可以

Employee[]employees = managers;//完全沒問題

但是如果我們在使用employees的時候在里面存放了一個new Employees,那么就會發生一個異常

這個jdk的bug可以查一下

https://blog.csdn.net/aitangyong/article/details/30274749

Java集合中toArray一般情況下都是Object[]數組,不過手動實現一個集合,有可能出問題,所以jdk采用這種方式避免了不必要的麻煩

就是避免這種情況:

ArrayList<Integer> integers = new ArrayList<>(0);
integers.add(1);
System.out.println(integers.toArray().getClass());
Integer[]integers_array = new Integer[2];
integers_array[0]=1;
integers_array[1]=2;
Class c = Arrays.asList(integers_array).toArray().getClass();
System.out.println(c);

Array.asList就是包裝一個視圖,里面使用add remove什么的都會拋一個異常


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM