この記事について
この記事はJava・JDK・OpenJDKの開発者であるStuart Marks氏(@stuartmarks)による記事 There is no such thing as a fail-safe Iterator in Java 1 を、ご本人の承諾を得た上で翻訳したものです。
Javaプログラムにて、コレクションの反復処理中に要素が変更された時の挙動を正確に理解するのにとても勉強になる記事だと思い、日本語に翻訳してQiitaに投稿させていただきました。
なお、原文で使用されている技術用語の訳し方の大部分は、OracleによるJava SE仕様書の日本語訳を参考にしています。
また、翻訳記事は初めてのため、読みづらい点や翻訳ミスが含まれる可能性があります。そのような点を見つけた際はコメントや編集リクエスト等でご指摘いただけるととても助かります。
本文
時折、Javaの「フェイルセーフな」イテレータについて言及されているのを目にすることがありますが、これは何を意味しているのでしょうか。
コレクションが反復処理されている最中にそのコレクションが変更(略して「並列変更」とも言います)された際に何が起こるのか、という文脈でこの言葉が使われます。それぞれのコレクションは、並列変更をハンドリングする際のポリシーが定義されています。注意すべき問題は、イテレータの「外部」からコレクションが直接変更された場合です。イテレータを通してのコレクションへの変更は、一部の例外はあるものの、一般的に認められているからです。
実は「フェイルセーフ」という言葉は、Java SEの仕様書のどこにも使われていません。その代わり、仕様書では並列変更に対する4つの異なるポリシーが定義されています。フェイルファスト(訳注:さっさとエラーにする)、弱一貫性、スナップショット、そして未定義です。
1. フェイルファスト
java.utilに含まれる並行でないコレクションのほとんどはフェイルファストなイテレータを持っています。例えば、ArrayListの仕様書を見てください。
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
(訳注:ArrayList)
このクラスのiteratorおよびlistIteratorメソッドによって返されるイテレータは、フェイルファストです。イテレータの作成後に、イテレータ自体のremoveまたはaddメソッド以外の方法でリストが構造的に変更されると、イテレータはConcurrentModificationExceptionをスローします。このように、並行して変更が行われると、イテレータは、将来の予測できない時点において予測できない動作が発生する危険を回避するために、ただちにかつ手際よく例外をスローします。
(訳注:以下、原文でJava SEの仕様書を引用している部分については、日本語版のJava SEの仕様書の該当部分を併せて引用します。)
2. 弱一貫性
ConcurrentHashMapなど、java.util.concurrentに含まれる並行処理コレクションの大部分は弱一貫性です。java.util.concurrentパッケージの仕様書では以下のように定義されています。
Most concurrent Collection implementations (including most Queues) also differ from the usual java.util conventions in that their Iterators and Spliterators provide weakly consistent rather than fast-fail traversal:
- they may proceed concurrently with other operations
- they will never throw ConcurrentModificationException
- they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.
(訳注:java.util.concurrent)
ほとんどの並行処理Collection実装(ほとんどのQueueを含む)は、それらのイテレータおよびスプリッテレータがフェイルファストのトラバーサルではなく弱一貫性を提供する点で、通常のjava.util規則とも異なります。
- 他のオペレーションとの並行処理が可能です。
- ConcurrentModificationExceptionをスローしません。
- 構築時に存在した要素を1回しかトラバースしないことが保証されており、構築後の変更を反映することも可能です(ただし保証はされません)。
3. スナップショット
3つ目のポリシーはスナップショットです。主な例としては、CopyOnWriteArrayListが挙げられます。
All mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.
The “snapshot” style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created.
(訳注:CopyOnWriteArrayList)
基になる配列の新しいコピーを作成することにより、すべての推移的操作(add、setなど)が実装されるArrayListのスレッド・セーフな変数です。
「スナップショット」スタイルのイテレータ・メソッドは、イテレータの作成時点での配列状態への参照を使用します。この配列がイテレータの有効期間中に変更されることは決してないため、干渉は不可能であり、イテレータはConcurrentModificationExceptionをスローしないことが保証されます。イテレータは、イテレータの作成以降のリストへの追加、削除、または変更を反映しません。
4. 未定義
4つ目のポリシーは「規定なし」です(これをポリシーと呼んで良いのであれば、ですが)。反復処理の最中にコレクションが更新された結果は定義されておらず、不整合が起こり得ます。例としては、レガシーなコレクションであるVectorやHashTable、または Vector.elements や Hashtable.elements、Hashtable.keysなどのEnumerationを返却するメソッド群が挙げられます。
もしEnumerationがVector.elementsメソッドから反復処理された場合、要素がスキップされたり、反復処理中に2回現れたり、もしくは予期せぬ例外が発生したり、などといった不自然な挙動が簡単に起こり得ます。
余談ですが、コレクションのフレームワークがJDK 1.2で登場したとき、VectorとHashtableが新しいインターフェイスを実装するために組み込まれました。例えばVectorであれば、Vector.elementsメソッドでEnumerationを取得できたり、Vector.iteratorメソッドでIteratorを取得できたり、といった具合です。Iteratorには、Enumerationには無いフェイルファストのポリシーがあります。Hashtableも同じくEnumerationやIteratorを提供しますが、Iteratorのみにフェイルファストのポリシーがあります。面白いことに、HashtableのIteratorとEnumerationは同じクラスとして実装されており、それがIteratorなのかEnumerationなのかを判別するフラグを保持しています。
結局、「フェイルセーフ」はどこに現れるのでしょうか?答えは「現れない」です。コレクションに対する並列変更のポリシーとして、「フェイルセーフ」という言葉はJava SEの仕様書には一切使われていないのです。そのため、イテレーターに対する「フェイルセーフ」の定義は、信頼性と一貫性が全くありません。一般的な「フェイルセーフ」の考え方をイテレーターに適用する人もいるかもしれませんが、それは認識の差が生まれやすく、誤解を招きやすく、さらには矛盾した実装を生む原因となり得ます。
「フェイルセーフ」という言葉をJavaのイテレーターを説明するときに使わないでください。代わりに、この記事で挙げた4つのポリシーを使ってください。
-
2016/7/28 時点の記事を翻訳しています。 ↩