ListIterator(双方向反復) — 位置ベースの操作
ListIterator は List を前後どちらの方向にも走査でき、走査しながら安全に「挿入・削除・置換」ができる反復子です。カーソルは常に next と previous の間にあり、「最後に返した要素」に対して remove/set が適用されます。使いどころと落とし穴を、例とテンプレートで整理します。
基本の仕組みとルール
- 双方向走査: hasNext/next と hasPrevious/previous で前後に移動できる。拡張 for ではできない「後ろ向き」走査が可能。
- 現在要素の考え方: ListIterator に「現在要素」はなく、カーソルは next で返る要素と previous で返る要素の間にある。remove/set は「最後に返した要素(直近の next または previous)」にだけ適用できる。
- 編集メソッドの規約:
- remove/set は「直近に返した要素」に対してのみ1回有効(add の直後は set 不可)。
- add はカーソル位置に挿入(オプション操作)。挿入後、previous でその要素が返る位置関係になることを理解して使う。
- インデックス取得: nextIndex/previousIndex で「次/前に返る要素のインデックス」を取得できる。
参考: ListIterator は Iterator を拡張し、双方向走査・位置ベース編集・カーソル位置取得を提供します。
補助資料のサンプルでは next/previous で前後走査、add による挿入例が確認できます。
基本コード例(前後走査と削除・置換・挿入)
import java.util.*;
public class ListIteratorBasics {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
ListIterator<String> it = list.listIterator();
// 前方向へ
while (it.hasNext()) {
String s = it.next();
if (s.equals("B")) it.set("X"); // 直近要素の置換(B→X)
}
// 後方向へ
while (it.hasPrevious()) {
String s = it.previous();
if (s.equals("A")) it.add("AA"); // カーソル位置に挿入(A の前後に注意)
if (s.equals("C")) it.remove(); // 直近要素の削除
}
System.out.println(list); // 例: [AA, A, X]
}
}
Java- ポイント:
- set/remove は「直近の next/previous で返した要素」にのみ適用可能。
- add はカーソル位置へ挿入され、すぐ previous すると今挿入した要素が返る位置関係になる。
- 備考: next/previous の往復で「位置を意識」して操作するのがコツ。
位置ベース操作の見取り図
- カーソルの概念: 長さ n のリストには n+1 のカーソル位置がある(要素間+両端)。カーソルは next と previous の間にあり、remove/set は最後に返した要素に作用する。
- add の効果: add はカーソル位置へ挿入。挿入後は、previous を呼ぶと挿入した要素が返る(その場で一歩「後ろ側」に入るため)。
- インデックスの把握: nextIndex と previousIndex で、次・前の返却対象のインデックスを確認できる。部分編集や範囲操作に便利。
レシピ集(よくある用途)
双方向で検索してから編集
List<String> list = new ArrayList<>(List.of("alpha","beta","gamma"));
// 後ろから置換
for (ListIterator<String> it = list.listIterator(list.size()); it.hasPrevious(); ) {
String s = it.previous();
if (s.startsWith("g")) it.set("G-" + s);
}
Java- ねらい: 末尾から条件検索→その場で置換(双方向ならでは)。
走査しつつ挿入(周辺にマーカーを付与)
List<String> list = new ArrayList<>(List.of("a","x","b"));
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String s = it.next();
if ("x".equals(s)) {
it.add("marker"); // x の直後へ挿入
}
}
Java- ねらい: 見つけた要素の直後に追加。add はカーソル位置挿入。
条件削除(remove の正しい適用)
List<Integer> nums = new ArrayList<>(List.of(1,2,99,3));
for (ListIterator<Integer> it = nums.listIterator(); it.hasNext(); ) {
int v = it.next();
if (v >= 90) it.remove(); // 直近要素の削除のみ有効
}
Java- ねらい: Iterator.remove と同様規約で安全に削除。
よくある落とし穴と回避策
- set/remove の呼び順違反: next/previous を呼んでいない状態で set/remove → IllegalStateException。必ず直前に要素を取得してから適用。
- add 直後の set: 仕様上、add の直後は set は未定義(呼べない)。要素を再取得してから set を使う。
- 拡張 for で編集: 拡張 for 中に list.remove/set/add すると不整合や例外の原因。編集が必要なら ListIterator を明示使用。
- ビューや非対応実装: subList の一部ビューやイミュータブル実装では編集メソッドが UnsupportedOperationException の場合あり。編集可能な実体へコピーして作業する。
- 位置混乱: 双方向と add を併用すると位置把握が難しくなる。nextIndex/previousIndex をログする、操作を小さく刻むのが安全。
テンプレート集(そのまま使える形)
- 前方向に走査して条件削除
for (ListIterator<T> it = list.listIterator(); it.hasNext(); ) {
T e = it.next();
if (shouldRemove(e)) it.remove();
}
Java- 後方向から走査して置換
for (ListIterator<T> it = list.listIterator(list.size()); it.hasPrevious(); ) {
T e = it.previous();
if (cond(e)) it.set(modified(e));
}
Java- 見つけたら直後に挿入
while (it.hasNext()) {
T e = it.next();
if (cond(e)) it.add(newElem);
}
Java- インデックスを参照しながら編集
while (it.hasNext()) {
int nextIdx = it.nextIndex();
T e = it.next();
// nextIdx を使って範囲ロジックを組む
}
Javaまとめ
- ListIterator は「双方向走査+その場編集(add/set/remove)+位置取得」ができる反復子。カーソルは next と previous の間にあり、remove/set は直近要素だけに適用できるという規約を守るのが肝です。
- 後ろ向き検索や「見つけた直後に挿入」など、位置ベースの編集が直感的に書ける一方、呼び順やカーソルの意味を誤ると例外・混乱につながります。ルールを押さえて、安全に使いこなしましょう。
