热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ArrayList迭代过程删除问题

一:首先看下几个ArrayList循环过程删除元素的方法(一下内容均基于jdk7):packagelist;importjava.util.Array

一:首先看下几个ArrayList循环过程删除元素的方法(一下内容均基于jdk7):

package list;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.prefs.Preferences;public class ListTest {public static void main(String[] args) {List list &#61; new ArrayList<>(Arrays.asList("a1", "ab2", "a3", "ab4", "a5", "ab6", "a7", "ab8", "a9"));// 迭代删除方式一for (String str : list) {System.out.println(str);if (str.contains("b")) {list.remove(str);}}// 迭代删除方式二int size &#61; list.size();for (int i &#61; 0; i ) {String str &#61; list.get(i);System.out.println(str);if (str.contains("b")) {list.remove(i);
// size--;
// i--;
}}// 迭代删除方式三for (int i &#61; 0; i ) {String str &#61; list.get(i);System.out.println(str);if (str.contains("b")) {list.remove(i);}}// 迭代删除方式四for (Iterator ite &#61; list.iterator(); ite.hasNext();) {String str &#61; ite.next();System.out.println(str);if (str.contains("b")) {ite.remove();}}// 迭代删除方式五for (Iterator ite &#61; list.iterator(); ite.hasNext();) {String str &#61; ite.next();if (str.contains("b")) {list.remove(str);}}}
}


方式一&#xff1a;报错 java.util.ConcurrentModificationException
方式二&#xff1a;报错&#xff1a;下标越界 java.lang.IndexOutOfBoundsException

    list移除了元素但size大小未响应变化,所以导致数组下标不对&#xff1b;
    list.remove(i)必须size--

    而且取出的数据的索引也不准确&#xff0c;同时需要做i--操作

方式三&#xff1a;正常删除&#xff0c;不推荐&#xff1b;每次循环都需要计算list的大小&#xff0c;效率低
方式四&#xff1a;
正常删除&#xff0c;推荐使用
方式五&#xff1a;报错&#xff1a; java.util.ConcurrentModificationException

二&#xff1a;如果上面的结果算错的话&#xff0c;先看下ArrayList的源码(add和remove方法)

ArrayList继承AbstractList,modCount是AbstractList中定义用于计算列表的修改次数的属性。

public class ArrayList extends AbstractList // AbstractList定义了&#xff1a;protected transient int modCount &#61; 0;

implements List, RandomAccess, Cloneable, java.io.Serializable

{

private static final long serialVersionUID &#61; 8683452581122892189L;

//设置arrayList默认容量 

 private static final int DEFAULT_CAPACITY &#61; 10;

//空数组&#xff0c;当调用无参数构造函数的时候默认给个空数组&#xff0c;用于判断ArrayList数据是否为空时

private static final Object[]EMPTY_ELEMENTDATA &#61; {};

//这才是真正保存数据的数组

private transient Object[] elementData;

//arrayList的实际元素数量 

private int size;

//构造方法传入默认的capacity 设置默认数组大小

public ArrayList(int initialCapacity) {

super();

if (initialCapacity <0)

throw new IllegalArgumentException("Illegal Capacity: "&#43;initialCapacity);

this.elementData &#61; new Object[initialCapacity];

}

//无参数构造方法默认为空数组 

public ArrayList() {

super();

this.elementData &#61; EMPTY_ELEMENTDATA;

}

//构造方法传入一个Collection&#xff0c; 则将Collection里面的值copy到arrayList 

public ArrayList(Collectionextends E> c) {

elementData &#61; c.toArray();

size &#61; elementData.length;

// c.toArray might (incorrectly) not return Object[] (see 6260652) 

if (elementData.getClass() !&#61; Object[].class)

elementData &#61; Arrays.copyOf(elementData, size, Object[].class);

}

 //下面主要看看ArrayList 是如何将数组进行动态扩充实现add 和 remove 

public boolean add(E e) {

ensureCapacityInternal(size &#43; 1); // Increments modCount!! 

elementData[size&#43;&#43;] &#61; e;

return true;

}

public void add(int index, E element) {

rangeCheckForAdd(index);

ensureCapacityInternal(size &#43; 1); // Increments modCount!! 

System.arraycopy(elementData, index, elementData, index &#43; 1,size - index);

elementData[index] &#61; element;

size&#43;&#43;;

}

private void ensureCapacityInternal(int minCapacity) {

// 通过比较elementData和EMPTY_ELEMENTDATA的地址来判断ArrayList中是否为空

// 这种判空方式相比elementData.length更方便&#xff0c;无需进行数组内部属性length的值&#xff0c;只需要比较地址即可。

if (elementData &#61;&#61; EMPTY_ELEMENTDATA) {

minCapacity &#61; Math.max(DEFAULT_CAPACITY, minCapacity);

}

ensureExplicitCapacity(minCapacity);

}

 private void ensureExplicitCapacity(int minCapacity) {

modCount&#43;&#43;;//ArrayList每次数据更新&#xff08;add,remove&#xff09;都会对modCount的值更新

//超出了数组可容纳的长度&#xff0c;需要进行动态扩展 

if (minCapacity - elementData.length > 0)

grow(minCapacity);

}


//这才是ArrayList动态扩展的点

private void grow(int minCapacity) {

int oldCapacity &#61; elementData.length;

//设置新数组的容量扩展为原来数组的1.5倍&#xff0c;oldCapacity >>1 向右位移&#xff0c;相当于oldCapacity/2, oldCapacity &#43; (oldCapacity >> 1)&#61;1.5*oldCapacity

int newCapacity &#61; oldCapacity &#43; (oldCapacity >> 1);

//再判断一下新数组的容量够不够&#xff0c;够了就直接使用这个长度创建新数组&#xff0c;

//不够就将数组长度设置为需要的长度 

if (newCapacity - minCapacity <0)

newCapacity &#61; minCapacity;

//判断有没超过最大限制

if (newCapacity - MAX_ARRAY_SIZE > 0)

newCapacity &#61; hugeCapacity(minCapacity);

//将原来数组的值copy新数组中去&#xff0c; ArrayList的引用指向新数组

//这儿会新创建数组&#xff0c;如果数据量很大&#xff0c;重复的创建的数组&#xff0c;那么还是会影响效率&#xff0c;

//因此鼓励在合适的时候通过构造方法指定默认的capaticy大小

elementData &#61; Arrays.copyOf(elementData, newCapacity);

}


private static int hugeCapacity(int minCapacity) {

if (minCapacity <0) // overflow 

throw new OutOfMemoryError();

return (minCapacity > MAX_ARRAY_SIZE) ?

Integer.MAX_VALUE :

MAX_ARRAY_SIZE;

}

// 删除方法

public boolean remove(Object o) {

// Object可以为null

if (o &#61;&#61; null) {

// 如果传入的对象是null,则会循环数组查找是否有null的元素,存在则拿到索引index进行快速删除

for (int index &#61; 0; index

if (elementData[index] &#61;&#61; null) {

fastRemove(index);

return true;

}

else {

// 对象非空则通过循环数组通过equals进行判断&#xff0c;最终还是要通过fastRemove根据索引删除

for (int index &#61; 0; index

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

// 快速删除方法&#xff1a;基于下标进行准确删除元素

private void fastRemove(int index) {

// 删除元素会更新ArrayList的modCount值

modCount&#43;&#43;;

// 数组是连续的存储数据结构&#xff0c;当删除其中一个元素&#xff0c;该元素后面的所有的元素需要向前移动一个位置

// numMoved 表示删除的下标到最后总共受影响的元素个数&#xff0c;即需要前移的元素个数

int numMoved &#61; size - index - 1;

if (numMoved > 0)

// 在同一个数组中进行复制&#xff0c;把(删除元素下标后面的)数组元素复制(拼接)到(删除元素下标前的)数组中

// 但是此时会出现最后那个数组元素还是以前元素而不是null

System.arraycopy(elementData, index&#43;1, elementData, index,numMoved);

// 经过elementData[--size] &#61; null则把数组删除的那个下标移动到最后&#xff0c;加速回收

elementData[--size] &#61; null; // clear to let GC do its work

}

 }


三&#xff1a;看下ArrayList进行foreach时所调用的迭代器(内部迭代器Itr)

/**

* An optimized version of AbstractList.Itr

*/

private class Itr implements Iterator<E> {

int cursor; // index of next element to return

int lastRet &#61; -1; // index of last element returned; -1 if no such

// expectedModCount是Itr特有的&#xff0c;modCount是公共的

// expectedModCount和modCount默认是两者相等的;ArrayList进行删除修改都会更新modCount的值

// 当ArrayList通过foreach进入它的内部迭代器Itr时&#xff0c;expectedModCount就被赋值为modCount的值&#xff0c;后续ArrayList进行增加或删除&#xff0c;只会更新modCount&#xff0c;而不会同步更新expectedModCount

// 所以迭代器根据这两个值进行判断是否有并发性修改

int expectedModCount &#61; modCount;


public boolean hasNext() {

return cursor !&#61; size;

}

// ArrayList通过foreach&#xff08;即增强for循环&#xff09;来循环是调用的是ArrayList中内部类Itr的next()

&#64;SuppressWarnings("unchecked")

public E next() {

checkForComodification();

int i &#61; cursor;

if (i >&#61; size)

throw new NoSuchElementException();

Object[] elementData &#61; ArrayList.this.elementData;

if (i >&#61; elementData.length)

throw new ConcurrentModificationException();

cursor &#61; i &#43; 1;

return (E) elementData[lastRet &#61; i];

}

// ArrayList中迭代器删除方法

public void remove() {

if (lastRet 0)

throw new IllegalStateException();

checkForComodification();


try {

ArrayList.this.remove(lastRet);

cursor &#61; lastRet;

lastRet &#61; -1;

// 通过ArrayList中foreach(即通过ArrayList内部Itr的迭代器)进行删除元素

// 此时会进行赋值 expectedModCount &#61; modCount;而不会抛出异常

expectedModCount &#61; modCount;

catch (IndexOutOfBoundsException ex) {

throw new ConcurrentModificationException();

}

}

final void checkForComodification() {

if (modCount !&#61; expectedModCount)

throw new ConcurrentModificationException();

}

}

对此应该差不多可以理解了。ArrayList通过foreach迭代是调用的其内部类Itr的next方法。如果通过foreach循环,要去除某些元素&#xff0c;只能通过迭代器删除。因为迭代器删除后会对expectedModCount &#61; modCount设置&#xff0c;不会再循环过程因为expectedModCount 和 modCount值不相等而抛出异常了。如果是通过ArrayList的删除则只会对modCount进行更新&#xff0c;但是ArrayList内部迭代器Itr的属性expectedModCount却没有得到更新&#xff0c;所以抛异常。

 


推荐阅读
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 本文介绍了一种自定义的Android圆形进度条视图,支持在进度条上显示数字,并在圆心位置展示文字内容。通过自定义绘图和组件组合的方式实现,详细展示了自定义View的开发流程和关键技术点。示例代码和效果展示将在文章末尾提供。 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 本文详细解析了客户端与服务器之间的交互过程,重点介绍了Socket通信机制。IP地址由32位的4个8位二进制数组成,分为网络地址和主机地址两部分。通过使用 `ipconfig /all` 命令,用户可以查看详细的IP配置信息。此外,文章还介绍了如何使用 `ping` 命令测试网络连通性,例如 `ping 127.0.0.1` 可以检测本机网络是否正常。这些技术细节对于理解网络通信的基本原理具有重要意义。 ... [详细]
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 出库管理 | 零件设计中的状态模式学习心得与应用分析
    出库管理 | 零件设计中的状态模式学习心得与应用分析 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 在Java基础中,私有静态内部类是一种常见的设计模式,主要用于防止外部类的直接调用或实例化。这种内部类仅服务于其所属的外部类,确保了代码的封装性和安全性。通过分析JDK源码,我们可以发现许多常用类中都包含了私有静态内部类,这些内部类虽然功能强大,但其复杂性往往让人感到困惑。本文将深入探讨私有静态内部类的作用、实现方式及其在实际开发中的应用,帮助读者更好地理解和使用这一重要的编程技巧。 ... [详细]
  • 本文探讨了如何利用Java代码获取当前本地操作系统中正在运行的进程列表及其详细信息。通过引入必要的包和类,开发者可以轻松地实现这一功能,为系统监控和管理提供有力支持。示例代码展示了具体实现方法,适用于需要了解系统进程状态的开发人员。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • 在Java Web服务开发中,Apache CXF 和 Axis2 是两个广泛使用的框架。CXF 由于其与 Spring 框架的无缝集成能力,以及更简便的部署方式,成为了许多开发者的首选。本文将详细介绍如何使用 CXF 框架进行 Web 服务的开发,包括环境搭建、服务发布和客户端调用等关键步骤,为开发者提供一个全面的实践指南。 ... [详细]
  • 在Java项目中,当两个文件进行互相调用时出现了函数错误。具体问题出现在 `MainFrame.java` 文件中,该文件位于 `cn.javass.bookmgr` 包下,并且导入了 `java.awt.BorderLayout` 和 `java.awt.Event` 等相关类。为了确保项目的正常运行,请求提供专业的解决方案,以解决函数调用中的错误。建议从类路径、依赖关系和方法签名等方面入手,进行全面排查和调试。 ... [详细]
  • Java排序算法详解:选择排序、插入排序、冒泡排序与递归实现
    本文详细解析了Java中的几种基础排序算法,包括选择排序、插入排序和冒泡排序,并探讨了递归在这些算法中的应用。选择排序通过每次找到未排序部分的最小值并将其置于已排序部分的末尾来实现;插入排序则通过逐步将每个元素插入到已排序序列的正确位置;而冒泡排序则是通过多次遍历数组,两两比较并交换相邻的元素,最终使较大的元素逐渐“冒”到数组末尾。文章还提供了具体的代码示例,帮助读者更好地理解和掌握这些算法的实现细节。 ... [详细]
author-avatar
灰包蛋啦_199
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有