这篇文章主要介绍在循环中动态删除集合(数组)元素遇到的问题:结果与实际期望的不符。待会看个例子,以及产生这个问题的原因和解决办法。
实例场景一:
public class Test {
public static void printList(List list) {
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
public static void main(String[] args) {
List<Boolean> list = new ArrayList();
list.add(new Boolean(true));
list.add(new Boolean(false));
list.add(new Boolean(false));
list.add(new Boolean(false));
for(int i=0;i<list.size();i++) {
if(list.get(i)) {
list.remove(i);
}
}
printList(list);
}
}
//output:
//false
//false
//false
这上面这个例子里,我们对判断list集合中的元素如果为true,就删掉这个元素。这时集合中只有第一个元素为true,所以删了它还有3个false元素,结果如我们所预想,接着对上面的list添加元素做些改变,在看看结果:
public class Test {
public static void printList(List list) {
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
public static void main(String[] args) {
List<Boolean> list = new ArrayList();
list.add(new Boolean(true));
list.add(new Boolean(true));//仅在这里做了处理
list.add(new Boolean(false));
list.add(new Boolean(false));
for(int i=0;i<list.size();i++) {
if(list.get(i)) {
list.remove(i);
}
}
printList(list);
}
}
//output:
//true
//false
//false
这一段代码更上面相比,仅仅将list集合中index = 1的false改成了true,照理说这一点小改动无伤大雅,但输出结果却与我们期盼的不一致:为什么不是false,false?为什么角标为0、1号的元素只删了一个,而不是全删呢?我们对循环过程进行断点调试,结果就一目了然了:由于角标为0的元素为true,所以它首当其冲的要被删掉,这一点没什么疑虑,但由于0号位元素被删除,导致list.size()由4变成了3,此时的list为(true,false,false)。在第二轮循环体中,i已经自加完毕,值变成了1,所以list.gei(1)访问的(true,false,false)中的第二个元素,第一个true被直接跳过去了,导致它没被判断删除,捡回了一命。而后面的循环又奈我(false)何,而这个循环只循环了3次。问题已经分析出来了,现在怎么解决这个问题呢?难道万能经典的for循环解决不了这个问题吗?要知道我对它情有独钟啊!好吧,我们分析解决问题思路:首先在之前的for循序中,每删除一个元素,list.size()就减1,但进入下轮循环时,i又已经自增了一个1,这样下去势必导致循环次数的较少,我们的目的是不管他是否删除了元素,他都要循环最原始我们想循环的次数(4)。于是在这里设下判断:当要删除集合元素时(list.size()-1),i就不自增,当不删除集合元素时,i才自增;这样就可以控制循环的次数更最原始的循环次数一致了:
public class Test {
public static void printList(List list) {
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
public static void main(String[] args) {
List<Boolean> list = new ArrayList();
list.add(new Boolean(true));
list.add(new Boolean(true));//仅在这里做了处理
list.add(new Boolean(false));
list.add(new Boolean(false));
for(int i=0;i<list.size();) {
if(list.get(i)) {
list.remove(i);
}else {
i++;
}
}
/*第二种写法
for(int i=0;i<list.size();) {
if(list.get(i)) {
list.remove(i);
continue;
}
i++;
}*/
printList(list);
}
}
//output:
//false
//false
通过这种写法,可以把{true,true,false,false}的第二个true的判断给补上去。 结果也就恢复正常了,看来写法丰富的for循环还是可以解决不少问题的。有时你遇到这种问题,想着换这一种遍历方式是不是就能避免呢,例如用迭代器(iterator).
public static void main(String[] args) {
List<Boolean> list = new ArrayList();
list.add(new Boolean(true));
list.add(new Boolean(true));//仅在这里做了处理
list.add(new Boolean(false));
list.add(new Boolean(false));
Iterator<Boolean> it = list.iterator();
while(it.hasNext()) {
if(it.next()) {
it.remove();
}
}
printList(list);
}
输出结果也是false,false。从这可以看出,迭代器帮我们解决了刚刚遇到的问题。而且它的写法更简单。看来我们又得庆幸多了一条解决之道。但在庆幸的同时,是否会好奇迭代器是怎么帮我们解决的呢?反正我闲的蛋疼,抱着能看懂多少算多少的态度去分析了源码,在此向大家汇报一下:
1.Iterator是一个接口,由于我们这里的list实际上是一个ArrayList,那我们就直接在ArrayList.class这里找,一下是类里面我们用到的几个方法:
public Iterator<E> iterator() {
return new Itr();
}
/**
* 能理解的就写注释,不能理解的不理会了,请原谅我太菜了。。
*/
private class Itr implements Iterator<E> {
int cursor; // 返回下一个元素的索引
int lastRet = -1; // 当前正在操作的元素的索引
int expectedModCount = modCount;
Itr() {}
//调这个方法时,注意cursor与lastRet值都没有变化,可以理解游标压根就不移动,底下的next,remove()
//才去改变这两个值,不更改集合的情况下:cursor初始为0,每次move()后,cursor加1,move()四次后,cursor=4,
//所以第五次进去while()循环判断,返回false。
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
//先判断hasNext(),再进入这个next(),底下这两个判断成立的情况下,hasNext()都会返回false,
//所以在hasNext()= true时,这两个if是不会进去的。
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//从这里可以看出每次move,cursor+1,此时cursor表示的是下一个元素的索引,所以它的值先行加了1,
//而我们要取的是集合中索引为0的元素,也就是lastRet = cursor(这个cursor是还未加1之前的值,这个很重要)=0
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//从实例理解:在我们的例子中要删除集合索引为1的元素,此时lastRet=1,删除后,我们将要操作的下一个元素
//索引cursor 赋值为 1;从而保证了下一次调next()方法时lastRet = 1(看上一行注释括号里的内容).因此当集合中只有3个元素时
//它还是从第二个元素操作起。跟我们for循环时,如果删除元素,那一次循环i就不自增,达到同一个效果。
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代器的实现过程在注释写明了,大家可以看看。至此,先前的问题也算水落石出了。还有一点值得一提:Java数组长度是不可变的,而js 里面数组长度是可以动态添加的,当你在动态删除js数组的内容时,也会遇到刚提到的问题,这是你可以考虑用上面提到的for循环写法来解决,毕竟这时Java提供的迭代器是帮不上忙的。