技術在學習中成長,源碼的世界沒有你想象的那麼複雜
前言
2018年的五月,開始java的源碼學習之旅,從簡單的角度去理解java的源碼,前幾天在學習交流中正好看了一下java集合的源碼,才發現源碼並沒有想象中的那麼難以理解,所以,源碼之旅從java的集合類開始咯。
本章的源碼版本爲:JDK1.8
類的關係
要理解ArrayList
的源碼,我們就需要從它的關係開始,ArrayList
繼承了AbstractList
,實現了List
接口,我們從UML圖可以看出:
虛線箭頭表示實現接口,實線箭頭表示繼承關係
ArrayList簡介
ArrayList
是java
中最常用的集合類了,說到ArrayList
,我們不得不說說LinkedList
,因爲他們都是從Collection
派生而來的,都是用來存放對象的序列的集合類,ArrayList
相比與LinkedList
有什麼優劣呢?
ArrayList
:優點:隨機訪問元素的速度快
缺點:從中間插入和移除元素比較慢
LinkedList
:優點:從中間插入和移除元素速度快
缺點:隨機訪問元素的速度比較慢
接下來我們就從源碼的角度去理解一下爲什麼有這些優缺點。
源碼分析
ArrayList的初始化
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;
// 空參的構造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // -- (1)
}
//指定初始容量的構造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity]; // -- (2)
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; // -- (3)
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//初始化給定一個集合的構造函數
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); // -- (4)
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); // -- (5)
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
從源碼中我們可以看出,ArrayList
其實底層使用的是數組
,transient Object[] elementData
這個變量就是用來存放對象的一個數組。
ArrayList
的空參構造函數:也就是默認的構造函數,當new ArrayList()
的時候調用這個方法,可以看出將elementData
變量地址指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
這個初始容量爲10,並且爲空的元素數組;如步驟(1)
ArrayList
的指定大小的構造函數:當初始化一個指定大小的new ArrayList(int)
的時候調用該方法,這個方法首先對initialCapacity
參數進行判斷,如果大於0,那麼創建一個指定大小的數組(2)
;如果等於0,創建一個空的數組(3)
;否則就判處異常;初始化給定一個集合的構造函數:如果初始化的時候,給定一個集合對象,那麼將這個集合轉換爲數組
(4)
,然後對這個數組的長度進行判斷,如果數組不等於0,那麼調用Arrays.copyOf(elementData, size, Object[].class)
方法(5)
,這個方法是一個核心方法,這個方法就是初始化一個大小爲等於當前數組的一個新的數組,然後將對象copy
到新的數組中,然後將內存地址指定給elementData
,從下面的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;
}
copyOf
方法首先判斷兩個對象的類型,如果類型一致,那麼直接創建一個同大小的數組;如果類型不一致,則調用Array.newInstance
指定類型進行初始化這個數組,當然,大小也是一致的; 最後調用System.arraycopy
將參數數組copy
到新的目標並返回。
ArrayList的常用方法之 add
我們先看一下源碼:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! -- (1)
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);//-- (2)
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //-- (3)
elementData[index] = element;
size++;
}
ArrayList有兩個add方法,第一個方法就是按順序將對象插入到尾部,第二個方法就是從中間插入對象。
add(E e)
方法首先會判斷數組的容量是否超過極限ensureCapacityInternal(size + 1)
,這個方法首先會進行容量的判斷,如果超過了極限,創建一個新的數組,大小是舊數組1.5倍,然後將舊數組中的對象全部拷貝到新的數組(1)
,等下會詳細解析這個方法。最後將參數對象插入到數組中,返回true。
add(int index, E element)
首先會調用rangeCheckForAdd(index)
進行index的是否越界的驗證(2)
,然後調用上一個方法中一樣的判斷容量是否超過極限的方法,下一步就是一個核心的方法System.arraycopy
,這個方法我們在ArrayList初始化中
已經講過了,但是這裏不太一樣:
elementData
: 源數組index
:源數組起始位置elementData
:目標數組index + 1
:目標數組起始位置size - index
:複製數組元素數目
從源碼中可以看出,當我們往一個ArrayList中間插入一個對象的時候,index索引處後面的索引往後移動一位,最後把索引爲index空出來,並將element賦值給它。這樣一來我們並不知道要插入哪個位置,所以會進行匹配那麼它的時間賦值度就爲n。
接下來看一下ensureCapacityInternal(size + 1)
這個方法的調用鏈:
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;
int newCapacity = oldCapacity + (oldCapacity >> 1);//oldCapacity >> 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);
}
ensureCapacityInternal(int minCapacity)
方法中判斷當前數組中的元素是否爲空,如果爲空則給定一個最大的值,然後調用ensureExplicitCapacity(minCapacity)
,這個方法主要是判數組容量是否超過極限,如果超過極限調用grow(int minCapacity)
,這個方法就是擴容方法,該方法會創建一個比原數組大1.5倍的新數組,然後將原數組中的所有對象copy
到新的數組中。
ArrayList的常用方法之 remove
remove
方法其實跟從中間插入對象的add
方法有很大的相似之處,如果我們刪除某一個元素,將index
開始後面的所有對象都往前移動一位,底層方法其實是複製一遍,所以刪除一個對象的複雜度和從中間插入一個對象是差不多的。
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;
}
ArrayList的常用方法之 get
ArrayList
的get
方法非常直觀的就能理解了,廢話不多說,直接看代碼:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
檢查index合法性,然後從數組中取出對象並返回,是不是很簡單呢?
總結
本章列舉了一些ArrayList常用的方法,瞭解到ArrayList底層其實是一個對象數組,以及從中間插入對象和移除對象比較慢的原因,從這些方法出發,理解ArrayList其他的方法會很簡單了。下一章講一講LinkedList的源碼。