今天在写一段很简单的代码,本来以为肯定没什么问题,然后直接跑的时候,吆,简单的一个List的操作报错了。仔细一看代码,确实有问题,但是一般真的是如果稍微不小心就会犯下面这种愚蠢的操作。

这里我把代码贴出来:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>(1);
list.add(1);
for (Integer a : list)
{
if (a == 1)
{
list.remove(a);
}
}
list.forEach(System.out::println);
}

上面的代码报错,我贴错误出来:







然后第一时间觉得有问题,这种遍历然后删除的操作应该要使用迭代器。然后我修改后改成了下面代码:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>(1);
list.add(1);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext())
{
Integer next = iterator.next();
if (next == 1)
{
list.remove(next);
}
}
list.forEach(System.out::println);
}

结果一运行同样报错,哎吆,一不小心还是直接去删除List了,然后再次修改才没了问题。最后修改的代码如下:

public static void main(String[] args)
{
List<Integer> list = new ArrayList<>();
list.add(1);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext())
{
Integer next = iterator.next();
if (next == 1)
{
iterator.remove();
}
}
list.forEach(System.out::println);
}

所以决心好好的研究下这个List在迭代过程中的删减操作为什么很容易报错。打开前面2次报错的代码异常出现的地方,可以清楚的看见报错的原因。

 

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

modCount,expectedModCount这2个变量是什么搞不懂,所以还是好好研究下吧。在这里我们看那段用迭代器遍历然后直接删除list中一个对象的那段代码,我们来研究一下:

打开JDK源码看一下Arraylist的add和remove操作:

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
} 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;
}
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; // clear to let GC do its work
}

前面我们已经知道,不管是使用fore循环还是说使用迭代器,List内部操作的都是hasnext()和next()方法。这里贴出源码:

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; public boolean hasNext() {
return cursor != size;
} @SuppressWarnings("unchecked")
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;
return (E) elementData[lastRet = i];
} 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();
}
} @Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
} final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

上面的源码人家JDK里面的注释写的已经很清楚了,这里我们来整理一下:

cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

lastRet:表示上一个访问的元素的索引

expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

modCount:AbstractList类中的一个成员变量,默认是0。

该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。

所以我们在对List做迭代操作的过程中,如果这个时候来添加或者删除这个List,这个时候expectedModCount是原来的初始化时候的modCount值,但是modCount这个时候都自增改变了值了,所以肯定就报错了。具体如下图:





OK,现在这个时候我们已经知道了报错的原因了,那么为什么在直接使用interator迭代器来删除就没问题呢?

我们自己看一下Iterator源码里面的remove()方法,就明白了。

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//下面这行是亮点,重新设值expectedModCount了。
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

好了,现在错误原因已经清楚了,和公司CTO聊了下这个问题,人家的原话是这么说的:

操作List在迭代的时候最好只是单纯的迭代,而不要试图去影响原来的那个List,特别是原来那个List的size长度。如果有需要最好也new一个新的list来处理过滤出来的数据最好。而且如果是单纯的要做查询就用Arraylist,如果要做插入和删除操作,最好用likenList。

我个人觉得说的很好很正确,以后编码时候如果要做过滤一个List这种操作时候,最好新new一个容器来搬数据,不要试图直接操作原来那个List。



List迭代过滤操作注意点的更多相关文章

  1. Stream01 定义、迭代、操作、惰性求值、创建流、并行流、收集器、stream运行机制

    1 Stream Stream 是 Java 8 提供的一系列对可迭代元素处理的优化方案,使用 Stream 可以大大减少代码量,提高代码的可读性并且使代码更易并行. 2 迭代 2.1 需求 随机创建 ...

  2. 如何对Backbone.Collection进行过滤操作

    首先我想说的是这篇文章的题目起的很怪,因为我不知道起个什么名字比较好.渲染列表是我们应用中最常见的操作了吧,在运用Backbone的应用中,我们一般会把列表作为一个Collcetion,然后指定一个V ...

  3. Trident的过滤操作

    1.过滤操作 只是判断某个tuple是否保留 无需跨网络,无需跨分区 不会改变tuple的结构,只是改变tuple的数量 2.需求 过滤掉不是订单的tuple. 其中订单中包含“IBEIfeng.gi ...

  4. stark组件之过滤操作【模仿Django的admin】

    一.先看下django的admin是如何实现过滤操作 首先在配置类中顶一个list_filter的列表,把要过滤的字段作为元素写i进去就可以了 class testbook(admin.ModelAd ...

  5. Fiddler过滤操作

    Fidller,不做过多的简介,其中的过滤操作肯定是绕不过去的.直接上图.

  6. 【SQL必知必会笔记(3)】SELECT语句的WHERE子句数据过滤操作

    上个笔记主要介绍了利用SELECT语句检索单个/多个/所有列,并利用DISTINCT关键字检索具有唯一性的值.利用LIMIT/OFFSET子句限制结果:以及利用ORDER BY子句排序检索出的数据,主 ...

  7. linux下拷贝命令中的文件过滤操作记录

    在日常的运维工作中,经常会涉及到在拷贝某个目录时要排查其中的某些文件.废话不多说,下面对这一需求的操作做一记录: linux系统中,假设要想将目录A中的文件复制到目录B中,并且复制时过滤掉源目录A中的 ...

  8. 通过反射,对javabean属性进行过滤操作

    /** * 根据属性名获取属性值 * @param fieldName 属性名 * @param o 传入对象 * @return */ private Object getFieldValueByN ...

  9. vue 对 v-for 中数组进行过滤操作

    之前写angularjs的时候,filter是可以直接在ng-repeat中使用.但是到了vue好像这个不起作用. 具体解决办法: 加一个计算属性: computed:{ filterData: fu ...

随机推荐

  1. Java学习笔记9(面向对象二:this、继承、抽象类)

    就近原则: 类中的方法中的变量和成员变量重名时,调用类的方法时候,生效的是方法中的变量,如果方法中没有定义变量,才会去成员变量中寻找 于是,提出了this关键字,为了区分重名问题 public cla ...

  2. 如何利用git shell提交代码到github

    在很早之前我根据找到的一些资料以及自己的实践总结了一篇如何将VS2015上的代码上传到GitHub上,后来我发现有小伙伴私信我,说跟我上面写的不一样,但是那段时间也比较忙,当我发现有人私信的时候差不过 ...

  3. 浏览器history操作实现一些功能

    返回拦截 功能:从广告进入到落地页后,给history增加一个页面,拦截返回动作 主要用到的是h5中的history对象,使用了pushState,和replaceState来操作. 并且加入了一些条 ...

  4. 使用GPA针对android应用的绘制分析

    使用GPA针对android应用的绘制分析 以前经常用GPA来perf端游的绘制,很多perf工具例如perfhud,pix对于加壳的程序总是束手无策,但是GPA却不受这个限制,可以自动HOOK 3D ...

  5. mongodb 创建用户

    1.创建管理员 2.授权认证 3.给使用的数据库添加用户 普通连接(非授权连接)数据库 mongod -f /usr/local/etc/mongod.conf 授权连接数据库 mongod -f / ...

  6. 【Zookeeper】Zookeeper安装配置

    本文演示使用三台Linux服务器安装Zookeeper,使用的是zookeeper-3.4.5版本,将zookeeper-3.4.5.tar.gz上传到linux服务器, 使用如下命令解压到/usr/ ...

  7. mmap函数实现

    转自:https://www.cnblogs.com/huxiao-tee/p/4660352.htmlmmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址 ...

  8. webAPI+angularJS文件上传和下载

    开发框架 前端 angularJS1.6 下载和保存文件FileSaver:https://github.com/eligrey/FileSaver.js/ 后端 .net WebAPI 1 导入Ex ...

  9. struts2(三)之表单参数自动封装与参数类型自动转换

    前言 对struts2的使用不外乎这几点,参数自动封装,拦截器的使用,数据校验,ognl表达(值栈和actionContext的讲解),struts2的标签,struts2的国际化, struts2的 ...

  10. [51nod1457]小K vs. 竹子

    小K的花园种着n颗竹子(竹子是一种茎部中空并且长得又高又快的热带植物).此时,花园中第i颗竹子的高度是hi米,并且在每天结束的时候它生长ai米. 实际上,小K十分讨厌这些竹子.他曾经试图去砍光它们,但 ...