[Java] 例外:ConcurrentModificationExceptionエラーの原因や対処法を解説
ConcurrentModificationExceptionは、コレクション(例:ArrayListやHashMap)を反復処理中に、そのコレクションが同時に変更された場合にスローされる例外です。
例えば、Iterator
を使ってリストを走査中に、remove()
やadd()
などでリストを変更すると発生します。
対処法としては、Iterator
のremove()メソッド
を使用して要素を削除するか、CopyOnWriteArrayList
などのスレッドセーフなコレクションを使用することで回避できます。
また、for-each
ループではなく明示的にIterator
を使うことも有効です。
- ConcurrentModificationExceptionの原因
- 具体的な発生例の紹介
- 例外の対処法の解説
- スレッドセーフなコレクションの活用法
- 安全なコレクション操作の方法
ConcurrentModificationExceptionとは
ConcurrentModificationException
は、Javaプログラミングにおいて、コレクションをイテレートしている最中に、そのコレクションが変更された場合に発生する例外です。
この例外は、主にIterator
を使用してコレクションを走査している際に、他のスレッドや同じスレッド内でコレクションの要素を追加、削除、または変更した場合にトリガーされます。
これにより、イテレーションの整合性が損なわれ、予期しない動作やエラーを引き起こす可能性があります。
特にマルチスレッド環境では、この例外に注意が必要です。
ConcurrentModificationExceptionの原因
コレクションの変更とイテレーションの関係
コレクションをイテレートしている最中に、そのコレクションに対して変更が加えられると、ConcurrentModificationException
が発生します。
イテレーションは、コレクションの要素を順に処理するプロセスですが、同時に要素を追加または削除すると、イテレーションの状態が不整合になり、例外がスローされます。
このため、コレクションの変更は、イテレーションが完了するまで行わないことが推奨されます。
Iteratorとfor-eachループの違い
Iterator
を使用する場合、明示的に要素を取得し、削除することができます。
一方、for-each
ループは、内部的にIterator
を使用していますが、要素の削除や追加を行うと、ConcurrentModificationException
が発生します。
for-each
ループは簡潔で便利ですが、コレクションの変更には注意が必要です。
マルチスレッド環境での問題
マルチスレッド環境では、複数のスレッドが同時に同じコレクションにアクセスし、変更を加えることがあります。
この場合、あるスレッドがコレクションをイテレートしている最中に、別のスレッドがそのコレクションを変更すると、ConcurrentModificationException
が発生します。
スレッド間の競合を避けるためには、適切な同期機構を使用することが重要です。
コレクションの内部構造の変更
コレクションの内部構造が変更されると、イテレーションの整合性が損なわれることがあります。
例えば、ArrayList
のサイズが変更されると、内部的な配列の参照が無効になる可能性があります。
このような変更がイテレーション中に発生すると、ConcurrentModificationException
がスローされます。
コレクションの変更は、イテレーションが完了してから行うことが望ましいです。
ConcurrentModificationExceptionの具体例
ArrayListでの発生例
ArrayList
を使用している場合、イテレーション中に要素を追加または削除すると、ConcurrentModificationException
が発生します。
以下のサンプルコードでは、ArrayList
をイテレートしている最中に要素を削除しようとしています。
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
System.out.println(item);
// イテレーション中に要素を削除
list.remove(item); // ここで例外が発生
}
}
}
A
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at App.main(App.java:10)
HashMapでの発生例
HashMap
でも同様に、イテレーション中に要素を変更するとConcurrentModificationException
が発生します。
以下のサンプルコードでは、HashMap
をイテレート中に要素を追加しています。
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("1", "A");
map.put("2", "B");
map.put("3", "C");
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
// イテレーション中に要素を追加
map.put("4", "D"); // ここで例外が発生
}
}
}
1: A
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1606)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1629)
at App.main(App.java:10)
for-eachループでの発生例
for-each
ループを使用している場合も、コレクションの変更によってConcurrentModificationException
が発生します。
以下のサンプルコードでは、for-each
ループ中に要素を削除しています。
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
System.out.println(item);
// イテレーション中に要素を削除
list.remove(item); // ここで例外が発生
}
}
}
A
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at App.main(App.java:10)
Iteratorを使った場合の発生例
Iterator
を使用することで、要素を安全に削除することができますが、イテレーション中にコレクションを変更すると、ConcurrentModificationException
が発生することがあります。
以下のサンプルコードでは、Iterator
を使って要素を削除していますが、コレクションを変更したために例外が発生します。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// イテレーション中にコレクションを変更
list.add("D"); // ここで例外が発生
}
}
}
A
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at App.main(App.java:13)
ConcurrentModificationExceptionの対処法
Iteratorのremove()メソッドを使う
Iterator
を使用する際には、remove()メソッド
を利用することで、イテレーション中に安全に要素を削除できます。
このメソッドは、現在のイテレーションの要素を削除するため、ConcurrentModificationException
を回避できます。
以下のサンプルコードでは、Iterator
を使って要素を削除しています。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// Iteratorのremove()メソッドを使用して要素を削除
if (item.equals("B")) {
iterator.remove(); // 安全に削除
}
}
System.out.println("残りの要素: " + list);
}
}
A
B
C
残りの要素: [A, C]
Concurrentなコレクションを使う
Javaには、スレッドセーフなコレクションが用意されています。
これらのコレクションは、複数のスレッドからの同時アクセスを安全に処理するために設計されています。
例えば、ConcurrentHashMap
やCopyOnWriteArrayList
などがあります。
これらを使用することで、ConcurrentModificationException
を回避できます。
CopyOnWriteArrayListの使用
CopyOnWriteArrayList
は、要素の変更が行われるたびに内部の配列をコピーするため、イテレーション中に要素を安全に追加または削除できます。
以下のサンプルコードでは、CopyOnWriteArrayList
を使用して要素を追加しています。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class App {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
System.out.println(item);
// イテレーション中に要素を追加
list.add("D"); // 例外は発生しない
}
System.out.println("最終的な要素: " + list);
}
}
A
B
C
最終的な要素: [A, B, C, D, D, D]
スレッドセーフなコレクションの選択
スレッドセーフなコレクションを選択することは、マルチスレッド環境での安全な操作において重要です。
Collections.synchronizedList()
やCollections.synchronizedMap()
を使用することで、通常のコレクションをスレッドセーフにすることができます。
以下のサンプルコードでは、synchronizedList
を使用しています。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("A");
list.add("B");
list.add("C");
synchronized (list) { // 明示的な同期
for (String item : list) {
System.out.println(item);
// 要素を削除する場合も同期が必要
if (item.equals("B")) {
list.remove(item); // 安全に削除
}
}
}
System.out.println("残りの要素: " + list);
}
}
A
B
C
残りの要素: [A, C]
Stream APIを使った安全な操作
Java 8以降では、Stream APIを使用することで、コレクションの要素を安全に操作できます。
Streamを使用すると、コレクションを変更することなく、フィルタリングやマッピングを行うことができます。
以下のサンプルコードでは、Streamを使用して要素をフィルタリングしています。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// Streamを使用して要素をフィルタリング
List<String> filteredList = list.stream()
.filter(item -> !item.equals("B")) // "B"を除外
.collect(Collectors.toList());
System.out.println("フィルタリング後の要素: " + filteredList);
}
}
フィルタリング後の要素: [A, C]
応用例:スレッドセーフなコレクションの活用
ConcurrentHashMapの使用例
ConcurrentHashMap
は、スレッドセーフなマップの実装であり、複数のスレッドからの同時アクセスを効率的に処理できます。
以下のサンプルコードでは、ConcurrentHashMap
を使用して、複数のスレッドが同時にマップに要素を追加しています。
import java.util.concurrent.ConcurrentHashMap;
public class App {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// スレッドを作成して要素を追加
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
map.put("Key" + i, "Value" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 6; i <= 10; i++) {
map.put("Key" + i, "Value" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最終的なマップ: " + map);
}
}
最終的なマップ: {Key1=Value1, Key2=Value2, Key3=Value3, Key4=Value4, Key5=Value5, Key6=Value6, Key7=Value7, Key8=Value8, Key9=Value9, Key10=Value10}
CopyOnWriteArrayListの使用例
CopyOnWriteArrayList
は、要素の変更が行われるたびに内部の配列をコピーするため、イテレーション中に要素を安全に追加または削除できます。
以下のサンプルコードでは、CopyOnWriteArrayList
を使用して、スレッドが同時に要素を追加しています。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class App {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
// スレッドを作成して要素を追加
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
list.add("Item" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 6; i <= 10; i++) {
list.add("Item" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最終的なリスト: " + list);
}
}
最終的なリスト: [Item1, Item2, Item3, Item4, Item5, Item6, Item7, Item8, Item9, Item10]
Collections.synchronizedListの使用例
Collections.synchronizedList()
を使用することで、通常のリストをスレッドセーフにすることができます。
以下のサンプルコードでは、synchronizedList
を使用して、複数のスレッドが同時にリストに要素を追加しています。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class App {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
// スレッドを作成して要素を追加
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
list.add("Element" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 6; i <= 10; i++) {
list.add("Element" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 明示的に同期を取る
synchronized (list) {
System.out.println("最終的なリスト: " + list);
}
}
}
最終的なリスト: [Element1, Element2, Element3, Element4, Element5, Element6, Element7, Element8, Element9, Element10]
よくある質問
まとめ
この記事では、JavaにおけるConcurrentModificationException
の原因や具体例、対処法について詳しく解説しました。
特に、コレクションをイテレートしている最中に変更が加えられることが、どのようにしてこの例外を引き起こすのかを理解することが重要です。
スレッドセーフなコレクションや適切な同期機構を利用することで、マルチスレッド環境でも安全にコレクションを操作できるようになりますので、ぜひ実際のプログラムに取り入れてみてください。