正向遍歷和反向遍歷


前言

之前搜索面試題的時候,出現了一個題:一個ArrayList在循環過程中刪除,會不會出問題,為什么?心里想的答案是肯定會有問題但是又不知道是為什么,在搜索到答案后,發現里面其實並不簡單,所以專門寫篇文章研究一下。

for循環正向刪除

先看示例,再解析原因:

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循環正向循環刪除
  11.  
    for (int i = 0;i < list.size();i++){
  12.  
    if (list.get(i).equals("222")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }

運行后,輸出結果:

[111, 222, 333, 444, 333]

發現,相鄰的字符串“222”沒有刪除,這是為什么呢?畫圖解釋:

解釋:刪除元素“222”,當循環到下標為1的元素的的時候,發現此位置上的元素是“222”,此處元素應該刪除,根據上圖中的元素移動可知,在刪除元素后面的所有元素都要向前移動一個位置,那么移動之后,原來下標為2的元素“222”,此時下標為1,這是在i = 1,時的循環操作,在下一次的循環中,i = 2,此時就遺漏了第二個元素“222”。

那么再做下一個測試,刪除元素“333”,結果將如何?

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循環正向循環刪除
  11.  
    for (int i = 0;i < list.size();i++){
  12.  
    if (list.get(i).equals("333")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }

運行結果:

[111, 222, 222, 444]

發現,沒有問題。原理在上一個測試已經說了,就不再贅述。

總結:for循環正向刪除,會遺漏連續重復的元素。

 for循環反向刪除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循環反向循環刪除
  11.  
    for (int i = list.size() - 1;i >= 0;i--){
  12.  
    if (list.get(i).equals("222")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }
 

運行結果:

[111, 333, 444, 333]

 發現,沒有問題。還是畫圖解釋:

反向刪除的時候,循環遍歷完了的元素下標才有可能移動(已經遍歷的元素,下標變化了也沒有影響),所以沒有遍歷的下標不會移動,自反向刪除會遍歷到所有的元素,正向會跳過一些元素。

總結:反向遍歷刪除,沒有問題(單線程)。

反向遍歷刪除(多線程)

  1.  
    public static void main(String[] args) {
  2.  
    ArrayList<String> list = new ArrayList<String>();
  3.  
    list.add( "111");
  4.  
    list.add( "222");
  5.  
    list.add( "222");
  6.  
    list.add( "333");
  7.  
    list.add( "444");
  8.  
    list.add( "333");
  9.  
     
  10.  
    Thread thread1 = new Thread() {
  11.  
    @Override
  12.  
    public void run() {
  13.  
    remove(list, "111");
  14.  
    try {
  15.  
    Thread.sleep( 1000);
  16.  
    } catch (InterruptedException e) {
  17.  
    e.printStackTrace();
  18.  
    }
  19.  
    }
  20.  
    };
  21.  
    Thread thread2 = new Thread() {
  22.  
    @Override
  23.  
    public void run() {
  24.  
    remove(list, "222");
  25.  
    try {
  26.  
    Thread.sleep( 1000);
  27.  
    } catch (InterruptedException e) {
  28.  
    e.printStackTrace();
  29.  
    }
  30.  
    }
  31.  
    };
  32.  
    Thread thread3 = new Thread() {
  33.  
    @Override
  34.  
    public void run() {
  35.  
    remove(list, "333");
  36.  
    try {
  37.  
    Thread.sleep( 1000);
  38.  
    } catch (InterruptedException e) {
  39.  
    e.printStackTrace();
  40.  
    }
  41.  
    }
  42.  
    };
  43.  
    // 使各個線程處於就緒狀態
  44.  
    thread1.start();
  45.  
    thread2.start();
  46.  
    thread3.start();
  47.  
    // 等待前面幾個線程完成
  48.  
    try {
  49.  
    thread1.join();
  50.  
    thread2.join();
  51.  
    } catch (InterruptedException e) {
  52.  
    e.printStackTrace();
  53.  
    }
  54.  
     
  55.  
    System.out.println(Arrays.toString(list.toArray()));
  56.  
    }
  57.  
     
  58.  
    public static void remove(ArrayList<String> list, String elem) {
  59.  
    // 普通for循環倒序刪除,刪除過程中元素向左移動,不影響連續刪除
  60.  
    for (int i = list.size() - 1; i >= 0; i--) {
  61.  
    if (list.get(i).equals(elem)) {
  62.  
    list.remove(list.get(i));
  63.  
    }
  64.  
    }
  65.  
    }

 

運行結果:

[444]

總結:多線程反向遍歷刪除,沒有問題。

Iterator循環刪除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //foreach循環刪除
  11.  
    Iterator iterator = list.iterator();
  12.  
    while (iterator.hasNext()){
  13.  
    if (iterator.next().equals("222")){
  14.  
    list.remove(iterator.next());
  15.  
    }
  16.  
    }
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }

運行結果:

  1.  
    Exception in thread "main" java.util.ConcurrentModificationException
  2.  
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  3.  
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
  4.  
    at joe.effective.Test.main(Test.java:20)

 這個問題就要借助源碼來分析了(JDK1.8):

  1.  
    public E remove(int index) {
  2.  
    rangeCheck(index);
  3.  
     
  4.  
    modCount++;
  5.  
    E oldValue = elementData(index);
  6.  
     
  7.  
    int numMoved = size - index - 1;
  8.  
    if (numMoved > 0)
  9.  
    System.arraycopy(elementData, index+ 1, elementData, index,
  10.  
    numMoved);
  11.  
    elementData[--size] = null; // clear to let GC do its work
  12.  
     
  13.  
    return oldValue;
  14.  
    }
  15.  
     
  16.  
    public boolean remove(Object o) {
  17.  
    if (o == null) {
  18.  
    for (int index = 0; index < size; index++)
  19.  
    if (elementData[index] == null) {
  20.  
    fastRemove(index);
  21.  
    return true;
  22.  
    }
  23.  
    } else {
  24.  
    for (int index = 0; index < size; index++)
  25.  
    if (o.equals(elementData[index])) {
  26.  
    fastRemove(index);
  27.  
    return true;
  28.  
    }
  29.  
    }
  30.  
    return false;
  31.  
    }
  32.  
     
  33.  
    private void fastRemove(int index) {
  34.  
    modCount++;
  35.  
    int numMoved = size - index - 1;
  36.  
    if (numMoved > 0)
  37.  
    System.arraycopy(elementData, index+ 1, elementData, index,
  38.  
    numMoved);
  39.  
    elementData[--size] = null; // clear to let GC do its work
  40.  
    }

可以看出,ArrayList的remove方法,一種是根據下標刪除,一種是根據元素刪除。

發現即使看了remove方法的源碼也不能找到報錯的原因,由於我們使用了Iterator迭代器,那么再看看迭代器的源碼,果不其然,就發現了問題所在:

  1.  
    private class Itr implements Iterator<E>
  2.  
    private class ListItr extends Itr implements ListIterator<E>
  1.  
    public void remove() {
  2.  
    if (lastRet < 0)
  3.  
    throw new IllegalStateException();
  4.  
    checkForComodification(); // 檢查修改次數
  5.  
     
  6.  
    try {
  7.  
    ArrayList. this.remove(lastRet);
  8.  
    cursor = lastRet;
  9.  
    lastRet = - 1;
  10.  
    expectedModCount = modCount;
  11.  
    } catch (IndexOutOfBoundsException ex) {
  12.  
    throw new ConcurrentModificationException();
  13.  
    }
  14.  
    }
  15.  
    final void checkForComodification() {
  16.  
    if (modCount != expectedModCount)
  17.  
    throw new ConcurrentModificationException();
  18.  
    }

Itr和ListItr是ArrayList的兩個私有內部類,Itr實現了Iterator接口,ListItr繼承了Itr類和實現了ListIterator接口。Itr類中也有一個remove方法,迭代器實際調用的也正是這個remove方法,上述源碼也就是這個方法的源碼。

由源碼的第二段代碼可以看出,這個remove方法中調用了ArrayList中的remove方法,在這個方法中我們注意到了expectedModCount變量和modCount變量,modCount在前面的代碼中也見到了,它記錄了ArrayList修改的次數,而前面的變量expectedModCount,這個變量的初值和modCount是相等的;同時在ArrayList.this.remove(lastRet);代碼面前,調用了檢查次數的方法checkForComodification(),這個方法做的事情很簡單,就是如果expectedModCount和modCount不相等,那么就拋出異常ConcurrentModificationException。

我們在用Iterator循環刪除的時候,調用的是ArrayList里面的remove方法,刪除元素后modCount會增加,expectedModCount則不變,這樣就造成了expectedModCount != modCount,那么就拋出異常了。

再用Iterator中的remove方法來測試:

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
     
  11.  
    Iterator iterator = list.iterator();
  12.  
    while (iterator.hasNext()){
  13.  
    if (iterator.next().equals("222")){
  14.  
    iterator.remove();
  15.  
    }
  16.  
    }
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }
運行結果[111, 333, 444, 333]

發現,刪除成功且沒有報錯。

什么原因呢?我們調用的了Iterator中的迭代器刪除元素,在這個方法中有:expectedModCount = modCount這樣一句代碼,所以當我們每刪除一次元素,就同步一次,所以調用checkForComodification()時,就不會報錯。如果換到多線程中,這個方法不能保證兩個變量修改的一致性,結果具有不確定性,所以不推薦這種方法。

總結:Iterator調用ArrayList的刪除方法報錯,Iterator調用迭代器自己的刪除方法,單線程不會報錯,多線程會報錯。

forEach循環刪除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //foreach循環刪除
  11.  
    for (String str : list){
  12.  
    if (str.equals("222")){
  13.  
    list.remove(str);
  14.  
    }
  15.  
    }
  16.  
     
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }
  1.  
    運行結果
  2.  
    Exception in thread "main" java.util.ConcurrentModificationException
  1.  
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  2.  
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
  3.  
    at joe.effective.Test.main(Test.java:20)

報錯。

foreach原理是因為這些集合類都實現了Iterable接口,該接口中定義了Iterator迭代器的產生方法,並且foreach就是通過Iterable接口在序列中進行移動。也就是說:在編譯的時候編譯器會自動將對for這個關鍵字的使用轉化為對目標的迭代器的使用

明白了原理就跟上述的Iterator刪除調用ArrayList中remove一樣了。

總結:forEach循環刪除報錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM