这篇文章主要介绍在循环中动态删除集合(数组)元素遇到的问题:结果与实际期望的不符。待会看个例子,以及产生这个问题的原因和解决办法。
实例场景一:
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提供的迭代器是帮不上忙的。