A little bit of everything

元・情報系大学院生の備忘録

【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. リストに 1, 2, 4, 6, 8, 9 という数を順に追加
  2. for文でリストの要素を1つずつ参照し、もし要素の値が偶数ならリストから削除
  3. リストに残った数を全て表示


だけど実行結果は以下のようになります。偶数の値が削除されていません。

1
4
8
9


このコードの問題点は、list.remove(i) を実行すると i 番目の要素が削除されるので、リストの i+1 番目以降の要素は1つずつ前にずれる、という点。

f:id:yuukiyg:20171023132319j:plain

つまり、このプログラムの「リストの i番目の要素が偶数だったらリストから削除する」の部分を絵にすると、以下のようになります。

f:id:yuukiyg:20171023132554j:plain

この例では、元々のリストの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.


結構簡単なミスですが、意外と気づかずにやってしまいそうです。
気をつけましょう!