概述
在遍及集合过程中对集合进行增删操作最好不要做,如果非要做可以利用迭代器,并发集合,或者同步代码。
单线程模式下直接使用迭代器提供的add/remove方法就行或者首先记录下标遍历完后进行增加/删除,多线程模式下建议使用同步或并发结合。
前言:
普通for循环遍历集合过程中进行删除,如果进行大量删除会报IndexOutOfBoundsException异常,如果少量删除可以成功删除,但是循环的次数会减少,造成结果不准确。
增强for循环遍历过程中进行删除,会报ConcurrentModificationException异常,并发修改异常。集合遍历时进行增删操作都需要留意是否会触发ConcurrentModificationException异常
以下以LList集合为例
普通for循环对集合增删操作问题
- 增加操作:能正常添加
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
for (int i = 0; i < list.size(); i++) {
if ("baidu".equals(list.get(i))) {
list.add("com");
}
}
System.out.println(list);
}
- 删除操作:有问题
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);
}
代码的原意思是删除所有元素,但结果如图,还剩两个元素
原因:每当删除一个元素时,集合的size方法的值都会减小1,这将直接导致集合中元素的索引重新排序,进一步说,就是剩余所有元素的索引值都减1,而for循环语句的局部变量i仍然在递增,这将导致删除操作发生跳跃。从而导致上述还剩两个元素。
应将循环中的代码修改如下
增强for循环对集合的增删操作问题
- 增加操作:并发修改异常
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
for (String s : list) {
if ("baidu".equals(s)) {
list.add("com");//ConcurrentModificationException
}
}
System.out.println(list);
}
使用javap -c 命令查看class文件的字节码
由上图红框圈起的部分不难发现,foreach 循环内部实际是通过 Iterator 实现的,以上代码等同于:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
String item = i.next();
if ("baidu".equals(item)) {
list.add("com");
}
}
System.out.println(list);
}
- 删除操作:并发修改异常,原理同增加操作一样
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
for (String s : list) {
if ("baidu".equals(s)) {
list.remove(s);
}
}
System.out.println(list);
}
迭代器对集合的增删操作问题
- 增加操作:因为Iterator没有add方法,此处我们使用List特有的迭代器ListIterator
- 正常添加
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
String str = it.next();
if ("baidu".equals(str)) {
it.add("com");
}
}
System.out.println(list);
}
- 删除操作:正常删除
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("baidu");
list.add("c");
list.add("d");
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
if ("baidu".equals(str)) {
it.remove();
}
}
System.out.println(list);
}
为什么迭代器对集合的增删操作不会出错
原因:迭代器内部还是利用ArrayList的添加删除函数进行操作,只不过操作只有会对相应的指针进行修改(下一个),如果进行了删除操作,集合整体长度变小,指向下一个的指针也会相应减小,所以再次访问下一个时就不会发生错误了。
源码:
terator 接口包含以下几个主要方法:
boolean hasNext();
// 检查是否有下个元素
E next();
// 获取下个元素
void remove();
// 移除当前指向的元素
ArrayList 的内部类 Itr 实现了 Iterator 接口,Itr 共有3个成员变量:
private class Itr implements Iterator<E> {
int cursor;
// 下一次遍历的元素的位置
int lastRet = -1;
// 前一次返回的元素的位置
int expectedModCount = modCount;
modCount 是 ArrayList 继承自 AbstractList 的一个变量。在AbstractList的源码注释中,是这样解释这个变量的:
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.
翻译成中文大意为:modCount 为 list 的结构变化次数,即 list 的元素数量变化次数。
查看 ArrayList 的源码,会发现在每次调用 add() 和 remove() 方法,都会进行 modCount++ 操作。
modCount 意为 list 的结构变化次数,而 expectedModCount 可被视为 Itr 内部记录的集合结构变化次数,该变量的作用如下。在 Itr 内部有一个 checkForComodification 方法,如下所示:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
当集合的实际结构变化次数 和 Itr 记录的变化次数不相等时,则抛出 ConcurrentModificationException 异常。而在 Itr 的 next() 方法 和 remove() 中都调用了 checkForComodification 方法。
ArrayList 内部 Itr 的 remove 方法的源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
// 调用集合的remove()方法
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
// 更新expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
实际上,调用 Itr 的 remove() 方法移除集合元素时,首先会调用 ArrayList 的 remove() 方法,再对 expectedModCount 进行更新。在下次调用 Itr.next() 方法获取下个元素时,不会出现 expectedModCount != modCount 的情况。
Iterator 为什么要检查集合的结构变化次数?
这其实是为了防止多线程并发修改集合,在一个线程遍历集合的同时,另一个线程同时增删集合元素,将无法保证数据的一致性,集合的遍历过程也将被打乱。采用 modCount 机制,在此情景下及时抛出异常,确保同一时间只会有一个线程修改或遍历集合,也即 fail-fast 策略。
最后
以上就是欣慰月光为你收集整理的普通for循环、增强for循环、迭代器对集合的增删操作问题的全部内容,希望文章能够帮你解决普通for循环、增强for循环、迭代器对集合的增删操作问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复