動態刪除集合遇到的一些問題理解
這篇文章主要介紹在循環中動態刪除集合(數組)元素遇到的問題:結果與實際期望的不符。待會看個例子,以及產生這個問題的原因和解決辦法。
實例場景一:
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提供的叠代器是幫不上忙的。
動態刪除集合遇到的一些問題理解