JAVA集合之ArrayList

一、前言
Java 集合類提供了一套設計良好的支援對一組物件進行操作的介面和類,JAVA常用的集合介面有4類,分別是:
- Collection:代表一組物件,每一個物件都是它的子元素
- Set:不包含重複元素的 Collection
- List:有順序的 collection,並且可以包含重複元素
- Map:可以把鍵(key)對映到值(value)的物件,鍵不能重複。
JAVA集合的類關係可以用圖表示如下:

- 實線邊框 是實現類,比如:ArrayList,LinkedList,HashMap等。
- 折線邊框 是抽象類,比如:AbstractCollection,AbstractList,AbstractMap等。
- 點線邊框 的是介面,比如:Collection,Iterator,List等
- 帶顏色 框的是工具類,比如:Collections,Arrays。
通過類圖我們知道,所有的集合都繼承了Iterator介面,也就是說,所有的集合都具有迭代器,可以通過迭代器去迴圈,事實上,很多集合的功能都是依託於迭代器去實現的。
二、ArrayList常用方法
方法名 | 功能 |
---|---|
size() | 返回當前集合的元素個數 |
isEmpty() | 判斷當前集合是否是空元素 |
contains(Object o) | 判斷當前集合是否包含某個物件 |
indexOf(Object o) | 獲取某個物件位於集合的索引位置 |
lastIndexOf(Object o) | 獲取最後一個位於集合的索引位置 |
get(int index) | 獲取指定位置的集合物件 |
set(int index, E element) | 覆蓋集合某個位置的物件 |
add(E e) | 新增物件進入集合 |
add(int index, E element) | 新增物件進入集合指定位置 |
remove(int index) | 移除索引位置的元素 |
remove(Object o) | 移除某個元素 |
我們一般使用ArrayList最常用的方法無非就是新增,查詢和刪除。我們接下來從原始碼層面上分析下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; //集合的大小 private int size; 複製程式碼
ArrayList構造方法
//指定容量構造方法 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); } } //預設無引數構造方法 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //指定集合構造方法 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) //官方的一個bug,c.toArray()可能不是一個object陣列,所以需要通過Arrays.copyOf建立1個Object[]陣列,這樣陣列中就可以存放任意物件了 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 複製程式碼
通過上面ArrayList的構造方法我們知道,ArrayList可以建立指定長度的list,也可以指定一個集合建立list,而預設的建立list是一個長度為10 的空陣列。
ArrayList的add()方法
public boolean add(E e) { ensureCapacityInternal(size + 1);// Increments modCount!! elementData[size++] = e; return true; } // 確認能否裝得下size+1的物件 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } //計算容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { //如果是預設長度,就比較預設長度和size+1,取最大值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code //如果容量大於陣列的長度 if (minCapacity - elementData.length > 0) //擴容 grow(minCapacity); } private void grow(int minCapacity) { //取陣列的長度 int oldCapacity = elementData.length; //計算新長度,新長度=舊長度+舊長度/2 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); } 複製程式碼
上面原始碼邏輯包括了,ArrayList的新增以及擴容,根據上面原始碼,我們知道,原來ArrayList的實際預設容量直到呼叫add()方法才會真正擴容到10,這裡通過new ArrayList()在記憶體分配的是一個空陣列,並沒有直接new Object[10],這樣設計是很巧妙的,可以節省很多空間。
ArrayList的add(int index, E element)方法
public void add(int index, E element) { //判斷是否越界 rangeCheckForAdd(index); ensureCapacityInternal(size + 1);// Increments modCount!! // 重新複製陣列,把index+1位置往後的物件全部後移 System.arraycopy(elementData, index, elementData, index + 1, size - index); //覆蓋index位置的物件 elementData[index] = element; size++; } 複製程式碼
ArrayList的指定位置新增物件方法,需要把指定位置後面的全部物件後移,所以這樣也是ArrayList相對於linkList新增耗時的地方。
ArrayList的get(int index)方法
public E get(int index) { rangeCheck(index); return elementData(index); } 複製程式碼
ArrayList的get(int index) 方法比較簡單,只有兩步,第一,檢查是否越界,第二,返回陣列索引位置的資料。
ArrayList的remove(int index)方法
public E remove(int index) { rangeCheck(index); //父類的屬性,用來記錄list修改的次數,後續迭代器中會用到 modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) //把index位置後面的元素左移 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } 複製程式碼
ArrayList 的remove(int index)方法主要分為 3步,第一步,判斷下標是否越界,第二步,記錄修改次數,並左移index位置後面的元素,第三,把最後位置賦值為null,用於快速垃圾回收。
ArrayList在迴圈中使用remove方法需要注意的問題
- for迴圈
List<Integer> integers = new ArrayList<>(5); integers.add(1); integers.add(2); integers.add(3); integers.add(4); integers.add(5); for (int i = 0; i < integers.size(); i++) { integers.remove(i); } System.out.println(integers.size()); 複製程式碼
這裡首先申明一個長度為5的ArrayList的集合,然後新增五個元素,最後通過迴圈遍歷刪除,理論結果輸出0,但是輸出的結果卻是2,為什麼呢?之前分析remove原始碼我們知道,ArrayList每刪除一次就會把後面的全部元素左移,以這5個元素為例,第一個正常刪除沒問題,刪除後,元素就只剩下[2,3,4,5],這個時候remove(1),還剩[2,4,5],再remove(2),剩下[2,4],後面再remove沒有元素了,所以最後size為2。
- foreach迴圈
List<Integer> integers = new ArrayList<>(5); integers.add(1); integers.add(2); integers.add(3); integers.add(4); integers.add(5); for (Integer integer : integers) { integers.remove(integer); } System.out.println(integers.size()); 複製程式碼
這段程式碼只是在上面的程式碼上面把for迴圈改成了foreach迴圈,這裡理論結果也是輸出0,但是最後卻報錯了,報錯資訊:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) 複製程式碼
這裡我們發現是ArrayList的迭代器方法,ArrayList$Itr說明是ArrayList的內部類Itr中checkForComodification出問題了,我檢視下原始碼,
//這是Itr內部的屬性,初始化等於ArrayList中的modCount int expectedModCount = modCount; final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 複製程式碼
看到這裡我們應該清楚了,我們呼叫ArrayList的remove方法,modCount的值修改了,但是迭代器中expectedModCount值沒有修改,所以就丟擲異常了。這時候肯定有人說,你這個是騙人的,我寫的foreach刪除就不會報錯!恩,對!有一種情況是不會報錯的,就是list中只有兩個元素時,比如這樣:
List<Integer> integers = new ArrayList<>(5); integers.add(1); integers.add(2); for (Integer integer : integers) { integers.remove(integer); } System.out.println(integers.size()); } 複製程式碼
這時候輸出結果為1,沒有報錯,為什麼呢?我們知道foreach是for迴圈的增強,內部是通過迭代器實現的,看到剛剛報錯的程式碼也證實了我們的猜想,所以,迭代器刪除,過程是這樣的,先判斷iterator.hasNext(),迭代器有沒有下一個元素,如果有就遍歷,遍歷就會呼叫iterator.next(),該原始碼如下:
public boolean hasNext() { return cursor != size; } 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]; } 複製程式碼
我們檢視原始碼發現,以上過程只有呼叫next()會進行 checkForComodification()
,當我們刪除了第一個元素時候,進入迴圈判斷,hasNext這個時候為false,不會呼叫next(),所以也就不會執行 checkForComodification()
,所以就能輸出1。