目录
目的
List遍历和删除
使用基本的for循环遍历删除
使用Iterator遍历,list进行删除
iterator原理与fail-fast原理
思考
目的
java中List的遍历和删除问题是个老生常谈的问题了,似懂非懂的很多,经得住几个为什么的很少,本文解释几个问题,有助于去贯通List中iterator的原理,fail-fast机制,这两点懂了可以应付各种遍历和删除的为什么?
List遍历和删除
使用基本的for循环遍历删除
很多开发人员已经被灌输了思想,必须使用iterator才可以遍历删除,其实不然,看如下代码
// 使用基本的for循环删除
public static void deleteFor() {
List<String> names = new ArrayList<>();
names.add("jack");
names.add("tim");
names.add("lili");
names.add("tim");
names.add("lucy");
names.iterator();
String name;
for (int i = 0; i < names.size(); ++i) {
System.out.println(name = names.get(i));
if ("tim".equals(names.get(i))) {
// 删除后list整体向前移动一位,i后退一位,保证不跳跃
names.remove(i--);
}
}
System.out.println(names.toString());
}
这种操作不会有问题,只是要注意避免游标变量的跳跃现象。
使用Iterator遍历,list进行删除
先看一段代码,这段代码我们使用增强for遍历,并且使用List的remove方法删除遍历元素
// 会不会产生想象中的ConcurrentModificationException?为什么?
public static void deleteStrongFor() {
List<String> names = new ArrayList<>();
names.add("jack");
names.add("tim");
names.add("lili");
for (String name : names) {
System.out.println(name);
if ("tim".equals(name)) {
names.remove(name);
}
}
System.out.println(names.toString());
}
// 会不会产生想象中的ConcurrentModificationException?为什么?
public static void deleteStrongFor() {
List<String> names = new ArrayList<>();
names.add("jack");
names.add("tim");
names.add("lili");
for (String name : names) {
System.out.println(name);
if ("lili".equals(name)) {
names.remove(name);
}
}
System.out.println(names.toString());
}
结果:第一段代码不会产生异常,第二段代码会产生异常。
原因:第一段代码不会产生异常的原理,没有触发fail-fast机制,iterator的原理和fail-fast机制不是那么完美!第二段代码会产生异常,fail-fast机制生效。
首先看下增强型for循环在编译后的代码,借助idea查看上述代码片段的.class文件,发现增强型for编译后就是使用while和iterator进行遍历的。所以要解释前面的现象就得分析一下iterator的原理。
public static void deleteStrongFor() {
List<String> names = new ArrayList();
names.add("jack");
names.add("tim");
names.add("lili");
Iterator var1 = names.iterator();
while(var1.hasNext()) {
String name = (String)var1.next();
System.out.println(name);
if ("tim".equals(name)) {
names.remove(name);
}
}
System.out.println(names.toString());
}
iterator原理与fail-fast原理
具体原理看ArrayList.Iterator的源码,如下
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
// 初始化时为0
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
// 初始化为list外部类的modCount
int expectedModCount = modCount;
Itr() {}
// 游标和size不相同则返回true
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();
// 返回结果之前,游标移到下一位置,等待下一次hasNext判断
cursor = i + 1;
// 返回结果之前,lastRet已确认
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
// 若删除操作,cursor被修改为上一个返回位置,保证了不跳跃
cursor = lastRet;
// next()方法会重置这个lastRet
lastRet = -1;
// 欺骗或者说完成fail-fast机制
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
- iterator中的cursor是下一个next()将要返回的元素的下标,初始化为0,第一次运行next返回第一个元素前,就已经移动到第二个元素的下标,就是1,依次类推。
- hasNext的判断依据 cursor!=size
- iterator中的 expectedModCount标记操作数,与List自身的modCount对比,不一样则ConcurrentModificationException,expectedModCount的值在获得list的iterator时初始化为list的modCount,本例中添加三个元素后,两者的值都为3
- fail-fast机制只在next和remove中进行判断
基于以上三点复现两代码的必要过程:
第一段代码
- 删除"tim"元素后,cursor=2,list的size=2,hasNext的结果为false,循环跳出不执行后面的next,所以没有意料中的异常
第二段代码
- 删除"tim"元素后,cursor=3,list的size=2,hasNext的结果为true,modCount=4(因为使用的时list的remove方法),expectedModCount=3(因为删除不是使用iterator进行的,expectedModCount不变)
- 由于hasNext为true,进入next方法,显而易见ConcurrentModificationException出现了
思考
- fail-fast的设计目的是快速甄别出逻辑上对集合的“并发”操作异常,而不是解决问题。否则使用基本的for可能抛出得是OutofBound之类的异常,出现此类异常时可能要花点时间去探究,fail-fast尽量将这种隐蔽的逻辑漏洞暴露出来,方便解决问题。
- 其实这种iterator.remove()是不是和基本的for循环中删除i--的机制很像,iterator接口注释中写明其第一个优点就是可以一边遍历,一边删除,但使用基本for也可以实现。
- fail-fast机制是否需要进一步加强在hasNext中?说这个的原因是,看下面代码
将list最后两个元素都设置为“tim”,那么在删除时,只会删除倒数第二个tim, 最后一个留下了,程序也没有异常,程序员也觉得没问题,其实还是有逻辑上的问题的。
public static void deleteStrongFor() {
List<String> names = new ArrayList<>();
names.add("jack");
names.add("tim");
names.add("tim");
for (String name : names) {
System.out.println(name);
if ("tim".equals(name)) {
// 这是个错误的示例
// 只删除了倒数第二个tim,程序依然正常运行
names.remove(name);
}
}
System.out.println(names.toString());
}
欢迎批评指正,共同进步!