1. 程式人生 > 程式設計 >解決在for迴圈中remove list報錯越界的問題

解決在for迴圈中remove list報錯越界的問題

最近在搞一個購物車的功能,裡面有一個批量刪除的操作,採用的是ExpandableListView以及BaseExpandableListAdapter。視乎跟本篇無關緊要,主要是為了記錄一個java基礎。迭代器iterator的使用

一、錯誤程式碼(主要就是購物車的批量刪除)

/**
   * 刪除選中的
   */
  public void delSelect() {
    int groupSize;
    if (mGropBeens != null) {
      groupSize = mGropBeens.size();
    } else {
      return;
    }
    for (int i = 0; i < groupSize; i++) {
      int childSize = mGropBeens.get(i).getChildBean().size();
      for (int j = 0; j < childSize; j++) {
        if (mGropBeens.get(i).getChildBean().get(j).isChecked()) {
          DataSupport.deleteAll(ShopcartBean.class,"product_id=?",mGropBeens.get(i).getChildBean().get(j).getProduct_id());
        mGropBeens.get(i).getChildBean().remove(j);
        }
      }
    }
    notifyDataSetChanged();
  }

分析一、其實就是一個迴圈遍歷list進行remove的操作,後來我分析了一下,錯誤的很明顯,就是你remove了這個list以後,list的長度就改變了,然後你繼續遍歷,就會報錯。感覺躲不了啊。錯誤有了,我覺得無法下手,後面既然remove不了,我就先刪除本地資料庫的方式,然後遍歷對data進行賦值。。。躲一下

/**
   * 刪除選中的
   */
  public void delSelect() {
    int groupSize;
    if (mGropBeens != null) {
      groupSize = mGropBeens.size();
    } else {
      return;
    }
    for (int i = 0; i < groupSize; i++) {
      int childSize = mGropBeens.get(i).getChildBean().size();
      for (int j = 0; j < childSize; j++) {
        if (mGropBeens.get(i).getChildBean().get(j).isChecked()) {
          DataSupport.deleteAll(ShopcartBean.class,mGropBeens.get(i).getChildBean().get(j).getProduct_id());
//          mGropBeens.get(i).getChildBean().remove(j);
        }
      }
    }
    //重新整理資料來源
    for (int i = 0; i < mGropBeens.size(); i++) {
      mGropBeens.get(i).getChildBean().clear();
      List<ShopcartBean> shopcartBeanlists = DataSupport.where("top_category_id=?",mGropBeens.get(i).getGroupId()).find(ShopcartBean.class);
      mGropBeens.get(i).setChildBean(shopcartBeanlists);
    }
    notifyDataSetChanged();
  }

分析二、寫了這樣以後還是感覺很不爽啊。明明我都迴圈了一遍知道要刪除這個物件了,還要等遍歷完,僅僅改變它的介面卡的data。感覺不爽,隨意的百度了下,發現有專門的解決方案,就是迭代器iterator

二、爭取的開啟方式

 /**
   * 刪除選中的
   */
  public void delSelect() {
    int groupSize;
    if (mGropBeens != null) {
      groupSize = mGropBeens.size();
    } else {
      return;
    }
    for (int i = 0; i < groupSize; i++) {
      Iterator<ShopcartBean> iterator = mGropBeens.get(i).getChildBean().iterator();
      while (iterator.hasNext()) {
        ShopcartBean shopcartBean = iterator.next();
        if (shopcartBean.isChecked()) {
          DataSupport.deleteAll(ShopcartBean.class,shopcartBean.getProduct_id());
          iterator.remove();
        }
      }
    }
    notifyDataSetChanged();
  }

分析三、發現這個玩意感覺還是很驚喜的,同時也感嘆自己基礎薄弱了。

一般迴圈for和foreach都需要先知道集合的型別,甚至是集合內元素的型別,即需要訪問內部的成員;iterator是一個介面型別,他不關心集合或者陣列的型別,而且他還能隨時修改和刪除集合的元素。他不包含任何有關他所遍歷的序列的型別資訊,能夠將遍歷序列的操作與序列底層的 結構分離。

基本使用模式就是:

 List<object> arr=xxx;//把你的list賦值過來
 Iterator it = arr.iterator();
  while(it.hasNext()){ 
  object o =it.next();//當前遍歷物件
  iterator.remove();//刪除或修改等等
  }

三、ok,先做記錄,後面繼續深入。have a nice day.這就是在一個list中刪除多個元素的正確解法。

補充知識:詳解ArrayList在遍歷時remove元素所發生的併發修改異常的原因及解決方法

本文將以“在遍歷中刪除”為著手點,在其基礎上進行原始碼分析及相關問題解決。modCount的含義、迭代器所包含的方法、為什麼會發生併發修改異常都將會在這篇文章中進行說明。

引入

這是一個併發修改異常的示例,它使用了迭代器iterator來獲取元素,同時使用ArrayList自身的remove方法移除元素(使用增強for迴圈去遍歷獲取元素亦會如此,增強for迴圈底層用的也是迭代器,enhanced for loop is nothing but a syntactic sugar over Iterator in Java)

public static void main(String[] args) {
 //請動手實踐執行一下
  List<String> list = new ArrayList<String>();
  list.add("a");
  list.add("b");
  list.add("c");
  list.add("d");
  list.add("e");
  Iterator<String> iterator = list.iterator();
  while (iterator.hasNext()) {
    String str = iterator.next();
    if (str.equals("a")) {
      list.remove(str);
    } else {
      System.out.println(str);
    }
  }
}

原因分析

ArrayList內部實現了迭代器Itr,如圖所示

解決在for迴圈中remove list報錯越界的問題

通過迭代器獲取元素時(iterator.next())會進行checkForComodification,原始碼如下

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;//cursor向後挪一位
  return (E) elementData[lastRet = i];//lastRet為當前取出的元素所在索引,後面會用到
}
final void checkForComodification() {/***再看這裡***/
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

modCount即此列表已被結構修改的次數。 結構修改是改變列表大小的那些修改(如增刪,注意列表大小是size而不是capacity),或以其他方式擾亂它,使得正在進行的迭代可能產生不正確的結果的那些修改。

而expectedModCount會在迭代器被創建出來時初始化為modCount,原始碼如下

private class Itr implements Iterator<E> {
  int cursor;    // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;
  
  //instance methods...
}  

是不是發現什麼端倪了呢?當呼叫remove時(進而呼叫fastRemove)即被視為結構修改,因此modCount的值是會發生變化的,這樣當程式再次通過iterator.next()獲取元素時,通過checkForComodification方法發現modCount變化了,而expectedModCount 依然是初始化時的值,因此丟擲ConcurrentModificationException。

讓我們來確認一下我們的想法,remove方法及fastRemove方法的原始碼如下

public boolean remove(Object o) {
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);/***看這行呼叫了fastRemove***/
        return true;
      }
  } else {
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

private void fastRemove(int index) {
  modCount++;/***再看這行modCount變化了(自增)***/
  int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData,index+1,elementData,index,numMoved);
  elementData[--size] = null; // clear to let GC do its work
}

這樣我們就對ArrayList在遍歷時remove元素所發生的併發修改異常有了一個明確的瞭解。

迭代器Itr的補充說明:size是陣列大小,cursor是下一元素的索引(雖然是下一元素的索引,但陣列開始索引是從0開始的,所以cursor預設初始化為0)陣列的最大索引一定是小於size的(size-1)索引=size還要取元素的話將會越界。

在說明“如何在不發生異常的情況下刪除資料”前,先說一下根據上述示例可能會產生的其他問題(如不感興趣也可跳過0.0)。

刪除不規範時所產生的其他問題

問題1.雖然remove的不規範,但是程式依然能夠執行,雖然不符合預期,但是沒有發生併發修改異常

如果我們刪除的不是a,而是d的話(最大為e),將會輸出a b c而不會發生併發修改異常,程式碼如下

while (iterator.hasNext()) {
  String str = (String) iterator.next();
  if (str.equals("d")) {//將原先的a改為d
    list.remove(str);
  } else {
    System.out.println(str);
  }
}

原因分析:因為刪除d時cursor由3變為4(從0起算),size由5變為4。因此hasNext返回true,並且迴圈結束,因此不會輸出e(迴圈結束也意味著不會通過next進行checkForComodification,所以不會引發異常)

問題2.看似不會發生併發修改異常,可實際卻發生了0.0

如果我們將要刪除的元素改為e,那麼當刪除e時cursor由4變為5,size由5變為4,5明明大於4了應該不會有下一元素了,不會進入迴圈通過next取元素了,可當這麼想著的時候,異常卻發生了,程式碼如下:

while (iterator.hasNext()) {
  String str = (String) iterator.next();
  if (str.equals("e")) {
    list.remove(str);
  } else {
    System.out.println(str);
  }
}

原因分析:這是由於iterator.hasNext()的原理導致,點選hasNext()檢視原始碼可發現,hasNext並不是由cursor < size來實現的而是通過cursor != size來實現的,這樣程式將再次進入迴圈取元素進而發生併發修改異常

public boolean hasNext() {
  return cursor != size;
}

如何在不報錯的情況下將元素刪除?

1.通過iterator獲取元素的同時使用iterator的remove方法移除元素,程式碼如下

while (iterator.hasNext()) {
  String str = (String) iterator.next();
  if (str.equals("a")) {
    iterator.remove();
  } else {
    System.out.println(str);
  }
}

通過Itr的remove原始碼可以發現(如下),它在每次刪除的同時還會更新expectedModCount為當前自增後的modCount,使得下次通過iterator.next()取元素時經得住checkForComodification校驗(試想一下如果沒有checkForComodification的話,程式將繼續迴圈下去,cursor本指向的是當前元素索引的下一位,但remove後資料將整體向前竄一位,從而導致cursor指向的索引位置對應的資料發生了偏差,上述問題2的情況時若沒有進行checkForComodification則還會發生NoSuchElementException異常,詳見上述next原始碼)。

lastRet的值為最新一次通過next獲取元素時,那個元素所對應的索引,這裡通過將cursor = lastRet,從而把cursor的索引向前移動了一位,繼而避免了取資料時的偏差(cursor 與 lastRet的關係詳見上面的next原始碼)

在這裡lastRet 會歸為-1(它所對應的元素已經被刪除了),這也是為什麼不能連續呼叫兩次迭代器的remove方法的原因,若執意如此,該方法將會丟擲IllegalStateException的異常

public void remove() {
  if (lastRet < 0)
    throw new IllegalStateException();
  checkForComodification();

  try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    lastRet = -1;
    expectedModCount = modCount;
  } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
  }
}

2.通過list自身的get方法獲取元素的同時通過自身的remove方法移除元素,程式碼如下

for (int i = 0;i<list.size();i++){
  String s = list.get(i);
  if ("a".equals(s)){
    list.remove(i);//進而呼叫fastRemove
    i--;//相當於cursor = lastRet;將返回的下一元素的索引 = 返回的最新元素的索引(當前元素索引)
  }else {
    System.out.println(s);
  }
}

該種情況下不會將expectedModCount修正為最新的modCount,同時也不會進行checkForComodification的檢查,若此時刪除並不修正當前索引的話,將會造成上述的資料偏差(遍歷條件中的list.size()儲存為固定值或連續呼叫list.remove(i)次數過多還可以發生索引越界異常0.0)

注意,不能保證迭代器的快速失敗行為,因為通常來說,在存在不同步的併發修改的情況下,不可能做出任何嚴格的保證。快速失敗的迭代器會盡最大努力丟擲ConcurrentModificationException。因此,編寫依賴於此異常的程式的正確性是錯誤的:迭代器的快速失敗行為應僅用於檢測錯誤。

add請參照remove的方式去查閱ArrayList中的內部類ListItr

以上這篇解決在for迴圈中remove list報錯越界的問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。