0Pricing
Java Academy · レッスン

Fail-Fast と Fail-Safe の Iterator

ConcurrentModificationException と fail-fast の動作を理解し、スナップショット Iterator を使う場面を学びます。

「Fail-Fast と Fail-Safe の Iterator」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。

フェイルファストとフェイルセーフのイテレーター

Java のイテレーターは、反復処理中に元のコレクションが変更されたときの動作によって分類されます。フェイルファストイテレーターは直ちに例外をスローし、フェイルセーフイテレーターはスナップショット上で処理を続けます。

フェイルファストイテレーター

標準的な Java コレクションの多く(ArrayList、HashMap、TreeSet)は、フェイルファストイテレーターを使用します。これらは変更回数を表すカウンターである modCount を追跡します。反復処理中に構造上の変更を行うと、ConcurrentModificationException が発生します。

import java.util.*;

List<String> list = new ArrayList<>(List.of("a", "b", "c"));

try {
    for (String s : list) {
        list.add("x"); // structural modification — throws!
    }
} catch (ConcurrentModificationException e) {
    System.out.println("ConcurrentModificationException caught!");
}
// Same for HashMap, TreeMap, HashSet, etc.

フェイルファストが存在する理由

フェイルファストの動作はデバッグに役立ちます。状態が壊れたまま反復処理を続けるのではなく、バグを直ちに表面化できるためです。問題を何も知らせずに状態が壊れる場合は、デバッグがはるかに困難になります。

// modCount is incremented on every structural change
// (add, remove, clear on ArrayList)
// Iterator records modCount on creation
// On each next(), Iterator checks: if current modCount != expected, throw CME

// This detects bugs like:
List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5));
for (Integer n : nums) {
    if (n == 3) nums.remove(n); // bug caught immediately
}
// ConcurrentModificationException — not silent wrong results

安全な削除: Iterator.remove()

Iterator に基づく反復処理中に安全に変更を行う唯一の方法は Iterator.remove() です。最後に返された要素を削除し、modCount も更新します。

List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5,6));
Iterator<Integer> it = nums.iterator();

while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) it.remove(); // safe: updates modCount
}

System.out.println(nums); // [1, 3, 5]

removeIf: 最新の方法

Java 8 では Collection に removeIf(Predicate) が追加されました。イテレーターを手動で管理せず、条件に一致する要素をより簡潔に削除できます。

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Ann", "Charlie"));

// Modern: removeIf handles iteration internally
names.removeIf(name -> name.startsWith("A"));
System.out.println(names); // [Bob, Charlie]

// Equivalent but verbose iterator approach:
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("A")) it.remove();
}

フェイルセーフイテレーター: CopyOnWriteArrayList

CopyOnWriteArrayList はフェイルセーフイテレーターを使用します。イテレーターの作成時点で取得したスナップショットを走査するため、変更が進行中の反復処理に影響しません。

import java.util.concurrent.CopyOnWriteArrayList;

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(List.of("a", "b", "c"));

for (String s : list) {
    list.add("x"); // no ConcurrentModificationException!
    System.out.print(s + " "); // prints a, b, c (snapshot)
}
System.out.println();
System.out.println(list); // [a, b, c, x, x, x] — modified copy

ConcurrentHashMap のイテレーター

ConcurrentHashMap は弱い一貫性を持つイテレーターを使用します。イテレーターの作成中または作成後のある時点の状態を反映しますが、CME は発生せず、並行して行われた更新が反映されるかどうかは保証されません。

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);

for (Map.Entry<String, Integer> e : map.entrySet()) {
    map.put("d", 4); // no CME — weakly consistent
    System.out.println(e.getKey() + "=" + e.getValue());
}
// May or may not print "d=4" — weakly consistent

Collections.synchronizedList

Collections.synchronizedList はフェイルセーフではありません。フェイルファストイテレーターを使用しますが、並行アクセスをブロックします。反復処理中は手動で同期する必要があります。

import java.util.*;

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.addAll(List.of("a", "b", "c"));

// Must synchronize during iteration!
synchronized (syncList) {
    for (String s : syncList) {
        System.out.println(s);
    }
}
// Without the synchronized block, CME is still possible from another thread

スナップショットイテレーターパターン

スナップショットを手動で作成すると、同期を行わずに変更可能なコレクションを安全に走査できます。

import java.util.*;

List<String> original = new ArrayList<>(List.of("a", "b", "c"));

// Take a snapshot copy before iterating
List<String> snapshot = List.copyOf(original);

for (String s : snapshot) {
    // Safe to modify original during snapshot iteration
    original.remove(s);
    System.out.println("Removed: " + s);
}
System.out.println(original); // []

CopyOnWrite のトレードオフ

CopyOnWriteArrayList はフェイルセーフですが、書き込みにはコストがかかります。変更のたびに完全なコピーが作成されるためです。

  • 適している場合: 読み取りが多く、書き込みが少ない場合(イベントリスナーのリストなど)
  • 適していない場合: 変更が頻繁な場合、大規模なコレクション

まとめ: フェイルファストとフェイルセーフ

比較のまとめ:

  • フェイルファスト: ArrayList、HashMap、TreeMap — 変更時に CME をスローし、modCount を確認
  • フェイルセーフ/弱い一貫性: ConcurrentHashMap、CopyOnWriteArrayList — CME は発生せず、スナップショットまたは並行して変更されるデータを走査
  • 安全な変更方法: Iterator.remove()、removeIf()、replaceAll()

実践: 期限切れセッションのクリーンアップ

適切なパターンを使用して、並行リストから期限切れのセッションを安全に削除します。

import java.util.*;
import java.util.concurrent.*;

class SessionManager {
    private final CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    record Session(String id, long expiresAt) {
        boolean isExpired() { return System.currentTimeMillis() > expiresAt; }
    }

    void add(Session s) { sessions.add(s); }

    // Safe to call from multiple threads while iterating
    void purgeExpired() {
        sessions.removeIf(Session::isExpired);
    }

    List<Session> active() {
        return sessions.stream().filter(s -> !s.isExpired()).toList();
    }
}

確認問題

反復処理中に ArrayList を変更すると、どの例外が発生しますか?

復習: フェイルファストとフェイルセーフのイテレーター

要点:

  • フェイルファストイテレーター(ArrayList、HashMap)は、並行して変更されると ConcurrentModificationException をスローします
  • フェイルセーフイテレーター(CopyOnWriteArrayList、ConcurrentHashMap)は、例外をスローせずに変更できます
  • フェイルファストの反復処理中に削除する安全な方法は Iterator.remove() だけです
  • removeIf() は、Iterator.remove() を手動で使用するよりも現代的で簡潔な方法です
  • CopyOnWriteArrayList: 読み取りが多く、書き込みが少ない場面に適しています
  • ConcurrentHashMap: 弱い一貫性を持ち、CME は発生しません。並行して行われた変更が反映されるかどうかは保証されません

よくある質問

「Fail-Fast と Fail-Safe の Iterator」レッスンは無料ですか?

はい。「Fail-Fast と Fail-Safe の Iterator」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。

「Fail-Fast と Fail-Safe の Iterator」で何を学びますか?

ConcurrentModificationException と fail-fast の動作を理解し、スナップショット Iterator を使う場面を学びます。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Java Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Fail-Fast と Fail-Safe の Iterator」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このJava Academyレッスンでコードを書いて実行できますか?

はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Iterable と Iterator の契約
  2. カスタム Iterator の実装
  3. ListIterator と双方向走査
  4. Fail-Fast と Fail-Safe の Iterator
← Java Academyに戻る