遍歷陣列刪除某元素的方法
從陣列中刪除元素是經常需要用到的情況,可能根據經驗你知道要從後往前刪除,但是你知道具體的原因嗎?本文通過簡單的解析讓你知其所以然。
假設一個需求,從陣列
["a", "bb", "bb", "ccc", "ccc", "ccc", "ccc"]
中刪除”bb”元素,即一個數組需要遍歷其中的元素,當該元素符合某個條件的時候從陣列中將該元素中刪除。
錯誤寫法
新手可能會直接寫出使用迭代器的以下程式碼:
寫法一:
public static void remove(ArrayList<String> list) {
for (String s : list) {
if (s.equals("bb")) {
list.remove(s);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
實際上,這段程式碼執行時會丟擲 ConcurrentModificationException
異常:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at ArrayListRemove.remove (ArrayListRemove.java:22)
at ArrayListRemove.main(ArrayListRemove.java:14)
- 1
- 2
- 3
- 4
- 5
我們暫時先不管它,換成普通的遍歷的寫法:
寫法二:
public static void remove(ArrayList<String> list) {
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("bb")) {
list.remove(s);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
這樣子寫執行時不報錯了,但是執行完之後陣列列印結果如下:
element : a
element : bb
element : ccc
element : ccc
element : ccc
- 1
- 2
- 3
- 4
- 5
可以發現並沒有把所有的 “bb” 刪除掉。
原始碼解析
我們看看這兩種寫法是怎樣出錯的。
首先看看方法二為什麼執行結果出錯,通過檢視 ArrayList 的 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
可以看到刪除元素時只刪除了第一個匹配到的元素。再檢視具體的 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; // Let gc do its work
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以看到,刪除時實際上是元素的移動。寫法二中,從前往後遍歷,index 遍歷到第一個 “bb”,刪除時即把從第二個 “bb” 及之後的元素拷貝到當前指向的位置,也就是第二個 “bb” 移動到了第一個 “bb” 的位置上,從而“刪除”了第一個 “bb”。接著,index 就跳過了當前位置,所以,第二個 “bb” 就被跳過了,也就不會被刪除了。
針對寫法二這種會引起錯誤結果的寫法,可以通過倒序遍歷的方式解決。
再回頭來看寫法一,發生了 ConcurrentModificationException
,這是因為迭代器內部維護了索引位置相關的資料,它要求在迭代過程中,容器不能發生結構性變化,所謂結構性變化就是 新增、插入 和 刪除 元素,而修改元素內容不算結構性變化。要避免該異常,就需要使用迭代器的 remove 方法。
迭代器怎麼知道發生了結構性變化,並丟擲異常呢?它自己的 remove 方法為何又可以使用呢?我們需要看下迭代器的工作原理。
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
// The "limit" of this iterator. This is the size of the list at the time the
// iterator was created. Adding & removing elements will invalidate the iteration
// anyway (and cause next() to throw) so saving this value will guarantee that the
// value of hasNext() remains stable and won't flap between true and false when elements
// are added and removed from the list.
protected int limit = ArrayList.this.size;
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor < limit;
}
@SuppressWarnings("unchecked")
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i >= limit)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//省略……
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
我們來看下 ArrayList 中 iterator 方法的實現,程式碼為:
public Iterator<E> iterator() {
return new Itr();
}
- 1
- 2
- 3
新建了一個 Itr 物件,而 Itr 是一個成員內部類,實現了 Iterator 介面,它有三個例項成員變數,為:
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
- 1
- 2
- 3
cursor 表示下一個要返回的元素位置,lastRet 表示最後一個返回的索引位置,expectedModCount 表示期望的修改次數,初始化為外部類當前的修改次數 modCount。
每次發生結構性變化的時候 modCount 都會增加,而每次迭代器操作的時候都會檢查 expectedModCount 是否與 modCount 相同,這樣就能檢測出結構性變化。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
- 1
- 2
而正確使用 iterator.remove()
方法卻不會引發異常,檢視原始碼得知:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可以看到 remove 呼叫的雖然也是 ArrayList 的 remove 方法,但它同時更新了 cursor, lastRet 和 expectedModCount 的值,所以它可以正確刪除而不引發異常。
從程式碼中注意到,呼叫 remove 之前需要 lastRet,所以呼叫 remove()
方法前必須先呼叫 next()
來更新 lastRet。
通過以上檢視原始碼分析,寫法一、二這兩種錯誤寫法做出相應的修正,可以得到正確寫法。
正確寫法
寫法三:倒序遍歷
public static void remove(ArrayList<String> list) {
// 這裡要注意陣列越界的問題,要用 >= 0 來界定
for (int i = list.size() - 1; i >= 0; i--) {
String s = list.get(i);
if (s.equals("bb")) {
list.remove(s);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
寫法四:
public static void remove(ArrayList<String> list) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("bb")) {
it.remove();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
引申及簡化
在這裡,做一個引申,對於陣列來說,可以使用一個更加簡單的寫法。也就是如果知道要刪除的元素是什麼就可以使用 ArrayList 物件的方法:remove
和 removeAll
。
public boolean remove(Object o);
public boolean removeAll(Collection<?> c);
- 1
- 2
可以看到,remove 方法可以傳一個物件進去,但它和正序遍歷一樣只會刪除第一個匹配到的元素,而 removeAll 方法可以刪除所有匹配的元素,但是傳入的需要一個容器類物件。
所以說要刪除所有的 “bb” 元素,那麼就應該這樣子寫。
寫法五:
public static void remove(ArrayList<String> list) {
// 構造一個 Collection
ArrayList<String> listTmp = new ArrayList<String>();
listTmp.add("bb");
list.removeAll(listTmp);
}
- 1
- 2
- 3
- 4
- 5
- 6
可以看到這樣子需要單獨構造一個 Collection 的寫法是很不優雅的,還好,Collections 類給我們提供了一個靜態方法 singleton()
:
public static <E> Set<E> singleton(E o);
- 1
它可以將一個普通物件轉換成一個容器物件,所以可以改寫成如下程式碼:
寫法六:
public static void remove(ArrayList<String> list) {
list.removeAll(Collections.singleton("bb"));
}
- 1
- 2
- 3
容器類的內容實在是太多了,可以多多檢視原始碼以及《Thinking in Java》容器相關內容。