解决在for循环中remove list报错越界的问题
作者:trebleZ 发布时间:2022-01-12 15:27:56
最近在搞一个购物车的功能,里面有一个批量删除的操作,采用的是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, "product_id=?", 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, "product_id=?", 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,如图所示
通过迭代器获取元素时(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
来源:https://blog.csdn.net/z_zT_T/article/details/70228813


猜你喜欢
- 在Scala中调用java的方法,很简单,直接导入传递参数就可以进行调用了.在Java中调用Scala的方法呢?经过测试,也是很简单,静态方
- RestTemplate设计是为了Spring更好的请求并解析Restful风格的接口返回值而设计的,通过这个类可以在请求接口时直接解析对应
- JNI中的java接口使用项目需求,需要在c++函数中监听相应的状态,并在java端进行一些列的处理。这个需要在JNI中写一个subscri
- 最近做通讯录小屏机 联系人姓名显示--长度超过边界字体变小/** * 自定义TextView,文本内容自动调整字体大小以适应TextVie
- 本文实例为大家分享了Handler实现倒计时功能的具体代码,供大家参考,具体内容如下1、需求1.1 实现目标当后台传递一个时间戳时,与当前系
- (一)单线程递归方式package com.taobao.test;import java.io.File;public class Tot
- 引言最近,因为开发的时候经改动依赖的库,所以,我想对 Gradle 脚本做一个调整,用来动态地将依赖替换为源码。这里以 android-mv
- 一、目的本篇文章的目的是记录本人使用flutter加载与调用第三方aar包。二、背景本人go后端,业余时间喜欢玩玩flutter。一直有一个
- Result 类型是许多编程语言中处理错误的常用方式,包括 C# 的 dotNext 库。在本文中,我们将通过例子回顾 C# 中 using
- 1.wait()方法和sleep()方法:wait()方法在等待中释放锁;sleep()在等待的时候不会释放锁,抱着锁睡眠。2.notify
- springboot嵌套子类使用在实际项目里,我们会使用到一个User用户含有子类Address、这种嵌套子类在开发中会遇到很多问题,现在主
- 一. 下载tomcat首先要到tomcat官网去下载安装包,官网下载地址如下:http://tomcat.apache.org/downlo
- 直接进入主题先来看一个栗子,假设现在有一个第三方dllnamespace TestLibrary1{ p
- 先写在前面,这说的Settings加载选项是指Settings这个应用显示在主界面的选项,这个修改需要对系统源码进行修改。Android 7
- 本文实例讲述了Java实现的RSA加密解密算法。分享给大家供大家参考,具体如下:import java.awt.AlphaComposite
- 今天遇到一个需求,需要处理通过接口传过来的一个参数,参数内容为一个拼接好的Url地址,且该地址还会携带了一些额外的参数,包括但不限于数字,字
- 介绍Java门面模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口,隐藏了复杂系统的实现细节,使得客户端可以更
- 前言好记性不如烂笔头,日期时间类那么花哨不如记下来多看两眼。提示:以下是本篇文章正文内容,下面案例可供参考一.日期时间类的包代码如下(示例)
- JVM运行原理首先从“.java”代码文件,编译成“.class”字节码文件,然后类加载器将“.class”字节码文件中的类给加载带JVM中
- 导入相关jar包1、junit<dependency> <groupId>junit</grou