【Java】for文でリストを回して要素を削除する際の注意
for文でリストを回して要素を削除しようとするなら、注意が必要です。
例えば、こんなコードがあったとする。
import java.util.ArrayList; public class Sample1 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(4); list.add(6); list.add(8); list.add(9); //要素の値が偶数だったら削除(?) for(int i=0; i<list.size(); i++){ if(list.get(i) % 2 == 0) list.remove(i); } //listがどんな状態か表示してみる。 for(int i=0; i<list.size(); i++){ System.out.println(list.get(i)); } } }
一見、このプログラムは次のような動作をするように見えます。
- リストに 1, 2, 4, 6, 8, 9 という数を順に追加
- for文でリストの要素を1つずつ参照し、もし要素の値が偶数ならリストから削除
- リストに残った数を全て表示
だけど実行結果は以下のようになります。偶数の値が削除されていません。
1 4 8 9
このコードの問題点は、list.remove(i) を実行すると i 番目の要素が削除されるので、リストの i+1 番目以降の要素は1つずつ前にずれる、という点。
つまり、このプログラムの「リストの i番目の要素が偶数だったらリストから削除する」の部分を絵にすると、以下のようになります。
この例では、元々のリストの2番目の「4」と4番目の「8」が飛ばされてしまっています。
このように、for文で i をぐるぐる回す中でリストのサイズが変化すると、思いどおりの動きにならないことがあります。
ちなみに上記のプログラムは、拡張for文を使って無理やり書くと、以下のようにも書けます。
import java.util.ArrayList; public class Sample2 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(4); list.add(6); list.add(8); list.add(9); //拡張for文で無理やり書いてみる int i = 0; for (Integer element: list) { if(element % 2 == 0) list.remove(i); i++; } //listがどんな状態か表示してみる。 for(int j=0; j<list.size(); j++){ System.out.println(list.get(j)); } } }
最初のコードと同じ動きをしそうだけど、これを実行すると以下のように
java.util.ConcurrentModificationException で怒られます。
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at Sample2.main(Sample2.java:15)
これは、イテレーションの中で Collection が変更されたときに投げられる例外らしいです。
では、どうやって書けばよいか?
Iterator を使うのがキレイです。
import java.util.ArrayList; import java.util.Iterator; public class Sample3 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(4); list.add(6); list.add(8); list.add(9); //イテレータを使って、このように書くのがキレイ。 Iterator<Integer> it = list.iterator(); while(it.hasNext()){ int i = it.next(); if(i % 2 == 0) it.remove(); } //listがどんな状態か表示してみる。 for(int i=0; i<list.size(); i++){ System.out.println(list.get(i)); } } }
実行結果
1 9
ちゃんと偶数だけ消えています。
Iteratorのリファレンスにも、「イテレーションの中でコレクションの要素を削除することができる」と書かれています。
Iterator (Java Platform SE 7 )
Iterators allow the caller to remove elements from the underlying collection during the iteration with well-defined semantics.
結構簡単なミスですが、意外と気づかずにやってしまいそうです。
気をつけましょう!