JavaでConcurrentModificationExceptionを回避する

1前書き

この記事では、 ConcurrentModificationException クラスについて説明します。

まず、それがどのように機能するのかを説明してから、それをトリガーするテストを使用してそれを証明します。

最後に、実用的な例を使用していくつかの回避策を試します。

2 ConcurrentModificationException をトリガする

基本的に、 ConcurrentModificationException は、繰り返しているものが変更されたときに フェイルファーストするために使用されます。 これを簡単なテストで証明しましょう。

@Test(expected = ConcurrentModificationException.class)
public void whilstRemovingDuringIteration__shouldThrowException() throws InterruptedException {

    List<Integer> integers = newArrayList(1, 2, 3);

    for (Integer integer : integers) {
        integers.remove(1);
    }
}

ご覧のとおり、反復を終了する前に要素を削除しています。それが例外の引き金となります。

3ソリューション

ときどき、繰り返しながらコレクションから要素を削除したいことがあります。このような場合は、いくつかの解決策があります。

3.1. 反復子を直接使用する

for-each ループは舞台裏で Iterator を使用しますが、あまり冗長ではありません。ただし、以前のテストで Iteratorを使用するようにリファクタリングした場合は、 remove()などの追加のメソッドにアクセスできます。

for (Iterator<Integer> iterator = integers.iterator(); iterator.hasNext();) {
    Integer integer = iterator.next();
    if(integer == 2) {
        iterator.remove();
    }
}

今、私たちは例外がないことに気づくでしょう。その理由は、 remove()メソッドが ConcurrentModificationExceptionを引き起こさないからです。繰り返し中に呼び出しても安全です。

3.2. 繰り返し中に削除されない

for-each ループを維持したい場合は、可能です。それは、要素を削除する前に、繰り返しが終わるまで待つ必要があるということです。繰り返して、削除したいものを toRemove リストに追加してみてください。

List<Integer> integers = newArrayList(1, 2, 3);
List<Integer> toRemove = newArrayList();

for (Integer integer : integers) {
    if(integer == 2) {
        toRemove.add(integer);
    }
}
integers.removeAll(toRemove);

assertThat(integers).containsExactly(1, 3);

これは問題を回避するもう一つの効果的な方法です。

3.3. removeIf() を使う

Java 8は Collection インターフェースに removeIf() メソッドを導入しました。

つまり、それを使って作業しているのであれば、関数型プログラミングのアイデアを使用して、同じ結果を再び得ることができます。

List<Integer> integers = newArrayList(1, 2, 3);

integers.removeIf(i -> i == 2);

assertThat(integers).containsExactly(1, 3);

この宣言スタイルは、私たちに最小限の冗長性を提供します。ただし、ユースケースによっては、他の方法がより便利な場合があります。

3.4. ストリームを使用したフィルタリング

関数型/宣言型プログラミングの世界に飛び込むとき、コレクションの変異について忘れることができます。代わりに、実際に処理されるべき要素に集中することができます。

Collection<Integer> integers = newArrayList(1, 2, 3);

List<String> collected = integers
  .stream()
  .filter(i -> i != 2)
  .map(Object::toString)
  .collect(toList());

assertThat(collected).containsExactly("1", "3");

前の例とは逆のことを行いました。除外するのではなく、含める要素を決定するための述語を提供しました。利点は、削除と並行して他の機能を連鎖できることです。この例では、機能的な__map()を使用していますが、必要に応じてさらに多くの操作を使用できます。

4結論

この記事では、繰り返しながらコレクションから項目を削除する場合に発生する可能性がある問題と、その問題を解決するための解決策をいくつか紹介しました。

これらの例の実装はhttps://github.com/eugenp/tutorials/tree/master/core-java-concurrency-collections[GitHubに追加]をご覧ください。これはMavenプロジェクトなので、そのまま実行するのは簡単なはずです。