ArrayList是日常開發中經常使用到的集合,其底層采用數組實現,因此元素按序存放。其優點是可以使用下標來訪問元素,時間復雜度是O(1)。其缺點是刪除和增加操作需要使用System.arraycopy()
來移動部分受影響的元素,時間復雜度為O(N)。同時ArrayList
由於是采用數組來存放數據,因此會有容量限制,在使用時需要擴容,當插入操作超出數組長度,就會進行擴容,擴容后數組的長度為原來的1.5倍,默認的數組長度是10。
為了更好的掌握ArrayList,因此閱讀並仿照ArrayList源代碼,實現一個具有增刪改查以及自動擴容功能的簡易版MyArrayList(代碼幾乎與ArrayList源碼一致)。
首先新建一個class,命名為MyArrayList
。
public class MyArrayList<E> {
}
由於ArrayList是通過數組來存儲元素的,因此我們定義一個Object類型的數組elementData
來存儲數據,再定義一個變量size
,用來記錄數組中的元素個數,其默認值為0。
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存儲元素的對象數組
private int size; //ArrayList存儲元素的個數
}
接下來就是構造函數,有兩種,第一種是未指定初始容量的構造函數,默認容量為10;第二種是在構造函數中傳入指定容量。
先說第一種構造函數,ArrayList在默認情況下,其容量為10。因此我們定義一個常量DEFAULT_CAPACITY = 10作為默認容量。同時,還定義一個常量數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}用於對elementData進行初始賦值。
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存儲元素的對象數組
private int size; //ArrayList存儲元素的個數
private final static int DEFAULT_CAPACITY = 10; //ArrayList的對象數組的默認初始容量
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList對象數組的默認初始化
/**
* 不指定初始容量的構造函數
*/
public MyArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
需要注意的是這里的默認容量10並不是在構造函數中直接使用,而是在第一次插入進行擴容時才會使用。
第二種構造函數,傳入指定的容量。根據傳入的容量參數,我們有以下三種結果:
①傳入的容量參數大於0:則以該參數為容量創建對象數組
②存入的容量參數等於0:則需要創建一個空的對象數組,因此定義一個常量數組EMPTY_ELEMENTDATA = {}用於傳入容量為0時的初始化。
③傳入的容量參數小於0:明顯這是非法的,因此拋出參數異常IllegalArgumentException()
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存儲元素的對象數組
private int size; //ArrayList存儲元素的個數
private final static int DEFAULT_CAPACITY = 10; //ArrayList的對象數組的默認初始容量
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList對象數組的默認初始化
private static final Object[] EMPTY_ELEMENTDATA = {}; //傳入容量為0時的初始化
/**
* 不指定初始容量的構造函數
*/
public MyArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 傳入指定初始容量的構造函數
* @param initialCapacity 傳入的初始化容量
*/
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("非法的容量: "+
initialCapacity);
}
}
}
好了,構造函數構建完畢,接下來就是增刪改查功能的實現,實現的方法如下:
//增,將元素放到數組末尾元素的后面,e為待插入元素,返回boolean
boolean add(E e)
//刪,刪除指定位置的元素,index為待刪除元素的位置,返回被刪除的元素
E remove(int index)
//改,替換指定位置的元素,index為被替換元素的位置,e為替換的元素,返回被替換的元素
E set(int index, E e)
//查,查詢指定位置的元素,index為查詢的位置,返回查到的元素
E get(int index)
首先是add(E e)方法,由於數組容量有限制,因此我們新增一個元素,都有可能要進行擴容,所以我們需要編寫一個函數ensureCapacityInternal來判斷是否需要自動擴容,若需要則進行擴容。
/**
* ArrayList的add方法
* 將元素放到數組末尾元素的后面
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自動擴容機制,傳入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
}
在ensureCapacityInternal函數中,需要傳入目前需要的最小容量。同時我們還要判斷對象數組elementData是否為空數組,若是,則將傳入的目前需要的最小容量與默認容量10進行對比,取其中的最大值作為本次擴容的容量。
/**
* 判斷原數組是否為空數組
* 是:則選默認容量和目前需要的最小容量二者中的最小值
* 否:則繼續往下判斷
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 為空數組,則將傳入的minCapacity與默認的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判斷
ensureExplicitCapacity(minCapacity);
}
接下來,我們判斷是否需要進行擴容。如果目前需要的最小容量大於原數組的長度,才進行擴容,否則不進行擴容,該功能寫入函數ensureExplicitCapacity。
/**
* 判斷是否需要進行擴容
* 如果目前需要的最小長度大於原數組的長度,才進行擴容
* 否則不進行擴容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超過原數組長度,才進行擴容,否則就不擴容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //擴容
}
}
然后,若進行擴容,則執行擴容函數grow。在grow中,我們需要進行如下操作:
①獲取原數組的容量oldCapacity,然后計算出值為oldCapacity1.5倍的新容量newCapacity
int oldCapacity = elementData.length;
//擴容為原來的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
②將擴容1.倍后的新容量newCapacity與目前需要的最小容量minCapacity進行對比,若新容量小於目前需要的最小容量,則新容量的值取目前需要的最小容量。
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
③將新容量newCapacity與所允許的數組的最大長度進行對比,數組最大長度定義為常量MAX_ARRAY_SIZE = Integer.MAX_VALUE,值為整數的最大值。如果新容量newCapacity大於數組最大長度MAX_ARRAY_SIZE ,則取目前需要的最小容量minCapacity與數組最大長度MAX_ARRAY_SIZE兩者中的最小值作為新容量newCapacity的值。
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
④使用Arrays.copyOf(原數組, 新長度)進行數組的復制,即實現數組擴容
elementData = Arrays.copyOf(elementData,newCapacity);
完成擴容任務的函數grow
如下:
/**
* 擴容函數:如何進行擴容(擴多少)
* ①擴容1.5倍
* ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
* ③若新的容量大於數組所允許的最大長度,則取需要的最小容量與數組所允許的最大長度
* 兩者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原數組長右移1位,即相當於除2,最后加原長度,則為擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容1.5倍后的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大於數組能夠允許的最大長度,則看傳入的最小容量與數組最大長度對比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原數組, 新長度),返回新數組。使用該函數完成自動擴容
elementData = Arrays.copyOf(elementData,newCapacity);
}
至此,就完成了新增時,判斷是否需要擴容,並且完成擴容功能。接下來我們只需要將新增元素插入數組元素末尾位置的下一個位置,並返回true即可。
boolean add(E e){
//1、自動擴容機制,傳入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、擴容完畢,將元素存入
elementData[size++] = e;
return true;
}
最終,新增add方法和自動擴容有關的函數編寫完成:
/**
* ArrayList的add方法
* 將元素放到數組末尾元素的后面
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自動擴容機制,傳入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、擴容完畢,將元素存入
elementData[size++] = e;
return true;
}
/**
* 判斷原數組是否為空數組
* 是:則選默認容量和目前需要的最小容量二者中的最小值,然后接着往下判斷
* 否:則直接繼續往下判斷
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 為空數組,則將傳入的minCapacity與默認的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判斷
ensureExplicitCapacity(minCapacity);
}
/**
* 判斷是否需要進行擴容
* 如果目前需要的最小長度大於原數組的長度,才進行擴容
* 否則不進行擴容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超過原數組長度,才進行擴容,否則就不擴容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //擴容
}
}
/**
* 擴容函數:如何進行擴容(擴多少)
* ①擴容1.5倍
* ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
* ③若新的容量大於數組所允許的最大長度,則取需要的最小容量與數組所允許的最大長度
* 兩者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原數組長右移1位,即相當於除2,最后加原長度,則為擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容1.5倍后的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大於數組能夠允許的最大長度,則看傳入的最小容量與數組最大長度對比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原數組, 新長度),返回新數組。使用該函數完成自動擴容
elementData = Arrays.copyOf(elementData,newCapacity);
}
接下來,就是刪除,remove方法。由於該方法傳入待刪除元素的位置索引index,因此需要檢查index
的范圍是否符合要求。編寫一個函數rangeCheck來檢查下標。
/**
* 檢查index范圍
* 超出范圍則拋出異常
* @param index 數組下標位置
*/
void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("哎呀,超出范圍了!");
}
若index沒有超出范圍,則接下來就是獲取索引對應的元素,獲取方式很簡單,就是elementData[index]即可。考慮到其他方法也會需要通過這樣方式來獲取對應位置的元素,因此我們將這個操作抽取出來,成為一個函數elementData(),用於獲取元素。
/**
* 返回數組中指定位置的元素
* @param index
* @return
*/
E elementData(int index){
return (E) elementData[index];
}
那么,目前remove方法前面兩個操作我們已經完成
E remove(int index){
//1、檢查index范圍
rangeCheck(index);
//2、獲取index對應的元素
E oldValue = elementData(index);
}
刪除index元素,需要把該位置后面的所有元素都向前移動一個位置。因此接下來我們就需要將index后面的元素向前移動一個位置。
具體做法是,先計算出需要移動的元素個數numMoved,用數組中最后一個元素的下標減去index即可獲得需要移動的元素個數:size-1-index。
然后利用System.arraycopy()來移動元素,該方法的用法如下:
System.arrayCopy(Object srcArray,int srcPos,Object destArray ,int destPos,int length)
①Object srcArray 原數組(要拷貝的數組)
②int srcPos 要復制的原數組的起始位置(數組從0位置開始)
③ Object destArray 目標數組
④ int destPos 目標數組的起始位置
⑤int length 要復制的長度
從原數組srcArray 取元素,范圍為下標srcPos到srcPos+length-1,取出共length個元素,存放到目標數組中,存放位置為下標destPos到destPos+length-1。
我們將原數組和目標數組都設為elementData,然后原數組的起始位置為index+1,目標數組的起始位置為index,要復制的長度設為元素個數numMoved。這樣就能做到將數組index位置后面的元素向前移動一位。
不過這樣做目標數組的最后一位元素依然是原來的數,因此我們需要將目標數組最后的元素置為null,並且由於是刪除,所以元素個數size需要減一。至此,刪除方法remove完成。
/**
* ArrayList的remove方法
* @param index 要刪除元素的位置
* @return 返回被刪除元素
*/
E remove(int index){
//1、檢查index范圍
rangeCheck(index);
//2、獲取index對應的元素
E oldValue = elementData(index);
//3、計算需要移動的元素的個數
int numMoved = size - 1 - index;
//4、將index后面的數往前移動一位
if (numMoved > 0){
System.arraycopy(elementData,index + 1, elementData, index, numMoved);
}
//5、把最后的元素置為null
elementData[--size] = null;
//返回被刪除元素
return oldValue;
}
增刪操作已完成,接下來就是改操作,set()方法。這個方法就比較簡單,具體的步驟如下:
①檢查index范圍
②獲取index位置的元素
③將index位置的元素,替換為傳入的元素
④返回原先index位置的元素
/**
* ArrayList的set
* @param index 需要修改的位置
* @param e 需要替換的元素
*/
E set(int index, E e){
//1、檢查index范圍
rangeCheck(index);
//2、獲取指定位置的元素
E oldValue = elementData(index);
//3、替換元素
elementData[index] = e;
//4、返回原先index位置的元素
return oldValue;
}
最后,就是查操作,get方法。該方法更為簡單,只需要先檢查index范圍,再獲取index位置的元素直接返回即可。
/**
* ArrayList的get方法
* @param index
* @return
*/
E get(int index){
//1、檢查index范圍
rangeCheck(index);
//2、獲取指定位置的元素
return elementData(index);
}
到這里,我們編寫的簡易版ArrayList的增刪改查操作就全部完成了。點進JDK1.8中ArrayList源碼可以看到,我們的上面的代碼幾乎與ArrayList源碼一模一樣。
最終這個簡易版ArrayList所有代碼如下:
public class MyArrayList<E> {
private int size; //ArrayList中實際元素的數量的size
private Object[] elementData; //ArrayList的對象數組
private final static int DEFAULT_CAPACITY = 10; //ArrayList的對象數組的默認初始容量
private final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE; //數組的最大長度,也就是整數的最大值
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList的對象數組的默認初始化
private static final Object[] EMPTY_ELEMENTDATA = {}; //傳入容量為0時的初始化
/**
* 不指定初始容量的構造函數
*/
public MyArrayList(){
elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 傳入指定初始容量的構造函數
* @param initialCapacity
*/
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("非法的容量: "+
initialCapacity);
}
}
/**
* ArrayList的add方法
* 將元素放到數組有效長度的末尾
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自動擴容機制,傳入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、擴容完畢,將元素存入
elementData[size++] = e;
return true;
}
/**
* 判斷原數組是否為空數組
* 是:則選默認容量和目前需要的最小容量二者中的最小值,然后接着往下判斷
* 否:則直接繼續往下判斷
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 為空數組,則將傳入的minCapacity與默認的初始容量DEFAULT_CAPACITY進行對比,取兩者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判斷
ensureExplicitCapacity(minCapacity);
}
/**
* 判斷是否需要進行擴容
* 如果目前需要的最小長度大於原數組的長度,才進行擴容
* 否則不進行擴容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超過原數組長度,才進行擴容,否則就不擴容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //擴容
}
}
/**
* 擴容函數:如何進行擴容(擴多少)
* ①擴容1.5倍
* ②若擴容1.5倍還不滿足需要的最小容量,則擴容長度為目前需要的最小容量
* ③若新的容量大於數組所允許的最大長度,則取需要的最小容量與數組所允許的最大長度
* 兩者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原數組長右移1位,即相當於除2,最后加原長度,則為擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容1.5倍后的新容量小於需要的最小容量,則新的容量即為傳入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大於數組能夠允許的最大長度,則看傳入的最小容量與數組最大長度對比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原數組, 新長度),返回新數組。使用該函數完成自動擴容
elementData = Arrays.copyOf(elementData,newCapacity);
}
/**
* ArrayList的remove方法
* @param index 要刪除元素的位置
* @return 返回被刪除元素
*/
E remove(int index){
//1、檢查index范圍
rangeCheck(index);
//2、獲取index對應的元素
E oldValue = elementData(index);
//3、計算需要移動的元素的個數
int numMoved = size - 1 - index;
//4、將index后面的數往前移動
if (numMoved > 0){
System.arraycopy(elementData,index + 1, elementData, index, numMoved);
}
//5、把最后的元素置為null
elementData[--size] = null;
//返回被刪除元素
return oldValue;
}
/**
* 檢查index范圍
* 超出范圍則報錯
* @param index
*/
void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("哎呀,超出范圍了!");
}
/**
* 返回數組中指定位置的元素
* @param index
* @return
*/
E elementData(int index){
return (E) elementData[index];
}
/**
* ArrayList的set
* @param index 需要修改的位置
* @param e 需要替換的元素
*/
E set(int index, E e){
//1、檢查index范圍
rangeCheck(index);
//2、獲取指定位置的元素
E oldValue = elementData(index);
//3、替換元素
elementData[index] = e;
//4、返回原先index位置的元素
return oldValue;
}
/**
* ArrayList的get方法
* @param index
* @return
*/
E get(int index){
//1、檢查index范圍
rangeCheck(index);
//2、獲取指定位置的元素
return elementData(index);
}
/**
* 獲取元素個數
* @return
*/
int size(){
return size;
}
}
我們測試一下,這個簡易版ArrayList
public static void main(String[] args) {
MyArrayList<String> myArrayList = new MyArrayList<>();
//增
myArrayList.add("hello");
myArrayList.add("word");
myArrayList.add("hello");
myArrayList.add("java");
//改
myArrayList.set(1,"####");
//刪
myArrayList.remove(2);
//查
for (int i = 0; i < myArrayList.size(); i++){
System.out.println(myArrayList.get(i));
}
}
測試結果如下:
hello
####
java