1. 程式人生 > >遍歷陣列刪除某元素的方法

遍歷陣列刪除某元素的方法

從陣列中刪除元素是經常需要用到的情況,可能根據經驗你知道要從後往前刪除,但是你知道具體的原因嗎?本文通過簡單的解析讓你知其所以然。

假設一個需求,從陣列 ["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 物件的方法:removeremoveAll

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》容器相關內容。