一、前言
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。
三、總結
- ArrayList可以指定容量例項化,也可以指定一個集合內容初始化,預設初始化長度是10(在執行add方法後才會給真正的空間),
- ArrayList指定位置新增和刪除,都會改變該位置之後的元素位置。
- ArrayList在迴圈中進行remove時候需要注意報錯和下標的問題,建議用迭代器刪除是最好的