はじめに
コレクションの要素を削除するには、拡張for文ではなくIteratorを使用する。
Javaを勉強した人なら一度は聞いたことがある知識だと思います。
本記事では、コレクションの要素を拡張for文で削除してはいけない理由
や、Iteratorの使い方
などについてまとめています。
目次
・コレクションとは
・ConcurrentModificationException
・Iterator
・拡張for文
・Iteratorの使い方
コレクションとは
コレクションとは、簡単に言えば複数の要素をまとめて扱うためのオブジェクト
です。
コレクションの代表的なインタフェースとして、要素(重複可)を順序付けて管理するList、一意な要素を順不同で管理するSet、先入れ先だしの構造を持ったQueue、一意なキーと値(重複可)のペアを管理するMapの4つがあります。
階層構造は以下の通りです。
おそらく一番馴染みがあるのは、Listインターフェースの実装クラスであるArrayListでしょうか。
ConcurrentModificationException
次にConcurrentModificationException(以下、CMEと省略)についてです。
名前にconcurrent(訳:同時)、modification(訳:変更)と付いていますが、マルチスレッドだけでスローされる例外ではなく、シングルスレッドにおいてスローされる場合もあります。
公式ドキュメントを見るとCMEについて次のように書かれています。(一部抜粋)
単一のスレッドが、オブジェクトの規約に違反する一連のメソッドを発行した場合、オブジェクトはこの例外をスローします。たとえば、フェイルファスト・イテレータを持つコレクションの反復処理を行いながら、スレッドがコレクションを直接修正する場合、イテレータはこの例外をスローします。
噛み砕いて説明します。
コレクションは基本的にイテレータというものを持っているため、反復処理が可能です。イテレータとは、プログラミング言語において集合的データ構造(配列やコレクション)の各要素に対する繰り返し処理
を表す概念です。イテレータにはフェイルファスト、弱一貫性、スナップショット、未定義という4つの分類があります。
このうち、ファイルファスト・イテレータを持つコレクションの反復処理中にコレクションの構造を変更(要素を削除するなど)すると、CMEがスローされる
ということです。
なお、Java.utilパッケージに属するほとんど全てのコレクションの持つイテレータは、フェイルファストです。
ちょっと難しいですが、並行処理で使う特殊なコレクション以外は基本的にフェイルファスト
です。並行処理に使われるコレクションはjava.util.concurrentパッケージで提供されています。
Iterator
Javaにおけるイテレータとはコレクション内の要素に順番にアクセスする手段
です。要素を指し示すカーソル
をイメージすると分かりやすいかもしれません。
Iteratorオブジェクトは、Iterableインタフェースのiterator()メソッド
によって取得できます。
階層構造は下図のようになっており、Iterableインタフェースを継承するコレクションの各クラスで、iterator()を呼び出すことでIteratorオブジェクトを取得することができます。
Iterableインタフェースを実装したオブジェクトはイテレータを持つ(Iteratorを使用できる)ため、次に説明する拡張for文の対象にすることができます。
拡張for文
拡張for文はコレクションの反復処理を簡単に記述すべくJava5から導入されたものであり、内部的にはIteratorを使用
しています。そのため、拡張for文はコンパイル時にIteratorを使用した処理へと書き換えられます。
ここでIteratorの実装を確認してみます。
Iteratorには、反復処理で次の要素を取り出すnext()メソッドというものがあり、next()メソッドは呼び出されるたびに内部でcheckForComodification()メソッドを実行します。
checkForComodification()メソッドでは、expectedModCount
(Iteratorによりコレクションが変更された回数)とmodCount
(実際にコレクションが変更された回数)という2つのフィールドの値を比較しており、2つの値に差異がある場合にCMEをスローします。
Iterator自身の持つremove()メソッドを使用して要素を削除する場合は常に
expectedModCount = modCount
となるため例外は発生しません。
ただし、Iteratorのremove()メソッドを拡張for文から直接呼び出すことはできない
ため、拡張for文でコレクションに変更を加えるとexpectedModCountとmodCountの値に差異が生じます。(Iteratorではコレクションを変更していないはずなのに、実際にはコレクションが変更されてしまっている状態になる。)
これによりCMEが発生するというロジックです。
そのため、例外を避けてコレクションの要素を削除したい場合には、Iteratorを明示的に使用するのが推奨されます。
前述したように、コレクションの反復処理中にIteratorのremove()メソッドを使用して要素が削除された場合に関しては、CMEがスローされることはありません。
以降は実例を交えながらIteratorの使い方について書いていきます。
Iteratorの使い方
例として、「ArrayListの要素を削除する」というのを題材にIteratorの使い方を見ていきます。
ちなみに、拡張for文ではなく普通のfor文を使う方法もNGです。理由については、こちらの記事の図解が分かりやすかったです。
そもそも、SetやMapなどの要素を順序付けて管理していないコレクションにおいては、インデックスが存在しないため単純なfor文は使用できません。
for文とは違いIteratorの汎用性は高く、ほとんどのコレクションに使用できます。コレクションの種類に関わらず統一的な記述ができる
というのはIteratorの大きなメリットです。
Iteratorの主なメソッドは以下の3つです。
boolean hasNext()
次の要素がある場合にtrueを返す。
E next()
次の要素を返す。
void remove()
イテレータによって最後に返された要素を削除する。
それでは、これら3つのメソッドを使ってサンプルコードを書いてみます。
先ほどご紹介した記事の例題をそのまま使わせていただきます。
例.
リストに1, 2, 4, 6, 8, 9 という数を格納し、偶数の値のみリストから削除せよ。
//指定の要素を格納したリストを生成
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 4, 6, 8, 9));
//Iteratorオブジェクトを取得
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
//イテレータが指し示す次の要素を取得
int i = it.next();
//取得した要素が偶数であれば削除する
if(i % 2 == 0) {
it.remove();
}
}