1. Iterable インターフェース
Java ではほとんどのコレクション(Map を除く)が Iterable インターフェースを実装しています。つまり、内部構造の詳細に立ち入らず、要素を一つずつ順番にたどれるということです。プログラマから見ると「コレクションには全要素を走査するための組み込みの方法がある」ように見えます。
インターフェース Iterable はちょうど 1 つのメソッドを定義します:
Iterator<E> iterator();
メソッド iterator() は Iterator 型のオブジェクトを返します。これはコレクションを一歩ずつ進む方法を知っている「アシスタント」です。これにより、馴染みのある for-each ループが動作します:
for (ElementType e : collection) {
// ...
}
実はその裏側で動いているのが Iterator です。さらに利点として、走査中に remove() を使って安全に要素を削除できます。通常のやり方でこれを行うと、簡単に ConcurrentModificationException を引き起こします。
2. Iterator インターフェース
Iterator は、要素を飛ばさずに、コレクションの走査順序に従って進める「ガイド」のような存在です。
| メソッド | 説明 |
|---|---|
|
次に走査する要素が残っているか? |
|
次の要素を返し、そこへ進む |
|
現在の要素を安全に削除する(不整合を起こさない) |
Iterator でコレクションを走査する例
import java.util.*;
public class IteratorDemo {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("猫をなでる");
tasks.add("宿題をやる");
tasks.add("ドラマを見る");
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
System.out.println("タスク: " + task);
}
}
}
ここで何が起きている?
- tasks.iterator() でイテレータを取得します。
- 次の要素がある間(hasNext() が true を返す間)、next() で取り出して出力します。
- イテレータが走査順序を管理してくれるため、コレクションが内部でどのように要素を保持しているかを知る必要はありません。
3. ループがあるのに、なぜ Iterator が必要?
Iterator を使えば、インデックスがないコレクション(たとえば Set)でも走査できます。コレクションの具体的な型に依存しない、汎用的な方法です。
安全に要素を削除する
よくある課題: コレクションを走査しながら一部の要素を削除する。これを for-each で行うと、エラーになることがあります:
for (String task : tasks) {
if (task.contains("猫")) {
tasks.remove(task); // ドカン! ConcurrentModificationException
}
}
なぜこうなるのか? イテレータによって開始された走査の最中に、コレクションが自分の構造を直接変更されるとは想定していないためです。
正しいやり方:
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("猫")) {
it.remove(); // すべて問題なく進む!
}
}
なぜ単純にインデックスを使えないのか?
すべてのコレクションにインデックスがあるわけではありません。たとえば HashSet や TreeSet には「5 番目の要素」という概念がありません。Iterator はいつでも使えます — そこが強みです。
4. for-each の詳説
拡張 for 文(for-each)は Java 5 で導入されました。本質的には、要素の走査をできるだけ簡単にするためのシンタックスシュガーです:
for (String task : tasks) {
System.out.println("タスク: " + task);
}
内部的にはコンパイラが iterator() を呼び、hasNext() で要素の有無を確認し、next() で取得します。まさに「リストの各タスクについて」と人間の言葉のように読めます。
for-each が向かない場面
- 走査中に要素を削除する必要がある(for-each では remove() を直接呼べない)。
- インデックスにアクセスする必要がある(例: 位置で要素を置き換える)。
- Map を扱っている(「キーと値」のペアなので、走査には専用のロジックが必要)。
5. Map の走査: コツと注意点
インターフェース Map は Iterable を直接実装していません。これは「キーと値」のペアの集合だからです。とはいえ、Map は走査に便利なビューを提供します。
キーで走査する
Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");
for (String login : users.keySet()) {
System.out.println("ログイン: " + login);
}
値で走査する
for (String email : users.values()) {
System.out.println("Email: " + email);
}
ペア(キーと値)で走査する
最も汎用的なのは entrySet() による走査です:
for (Map.Entry<String, String> entry : users.entrySet()) {
System.out.println("ログイン: " + entry.getKey() + ", Email: " + entry.getValue());
}
豆知識: Entry は Map の入れ子のインターフェースで、getKey() と getValue() を備えています。これにより、ペアの両方の値をすぐ取得できます。
Iterator を使って走査する
Iterator<Map.Entry<String, String>> it = users.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
// 要素を安全に削除することもできる:
if (entry.getKey().startsWith("v")) {
it.remove();
}
}
6. 実例: コレクション走査がアプリでどう役立つか
例: ユーザーの全タスクを表示
List<String> tasks = new ArrayList<>();
tasks.add("宿題をやる");
tasks.add("猫をなでる");
tasks.add("ドラマを見る");
System.out.println("今日のあなたのタスク:");
for (String task : tasks) {
System.out.println("- " + task);
}
次に、"猫" という語を含むタスクをすべて削除します:
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("猫")) {
it.remove();
}
}
System.out.println("残ったタスク:");
for (String task : tasks) {
System.out.println("- " + task);
}
例: Set でユニークなログインを走査する
Set<String> logins = new HashSet<>();
logins.add("vasya");
logins.add("petya");
logins.add("masha");
for (String login : logins) {
System.out.println("ユーザー: " + login);
}
注意: Set の出力順序は任意です!
例: Map を走査してユーザーを表示
Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");
for (Map.Entry<String, String> entry : users.entrySet()) {
System.out.println("ログイン: " + entry.getKey() + ", Email: " + entry.getValue());
}
7. Iterator.remove(): 要素を安全に削除する
初心者に非常に多いミスの一つは、for-each で走査しながら要素を削除しようとすることです。イテレータは remove() によってこの問題を解決します。
仕組み:
- it.remove() を呼ぶと現在の要素、すなわち直近の next() が返した要素が削除されます。
- これは安全で、コレクションは ConcurrentModificationException を投げません。
例:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int n = it.next();
if (n % 2 == 0) {
it.remove(); // 偶数をすべて削除する
}
}
System.out.println(numbers); // [1, 3, 5]
コレクション走査のイメージ図
+---------+ +---------+ +---------+
| Element | --> | Element | --> | Element | ...
+---------+ +---------+ +---------+
^ ^
| |
next() next()
Iterator は、hasNext() が false を返すまで要素を「一歩ずつ」進みます.
9. Iterator とコレクション走査でよくあるミス
ミス No.1: for-each で走査中にコレクションを変更する。
要素を for-each の中で直接削除しようとすると、ConcurrentModificationException を招きます:
for (String task : tasks) {
if (task.contains("猫")) {
tasks.remove(task); // ドカン! ConcurrentModificationException
}
}
Iterator とその remove() を使いましょう。
ミス No.2: next() より前に remove() を呼ぶ。
まず next() で現在の要素を取得する必要があります。そうしないと、イテレータは何を削除すべきか分かりません。
Iterator<String> it = tasks.iterator();
it.remove(); // エラー!まず next() が必要
ミス No.3: Map を for-each で直接走査しようとする。
Map は Iterable を直接実装していません — keySet()、values()、または entrySet() を使いましょう。
Map<String, String> users = new HashMap<>();
// for (String entry : users) { ... } // エラー: これはできない
for (Map.Entry<String, String> e : users.entrySet()) {
// 正しい
}
ミス No.4: イテレータで走査中に、イテレータ経由ではなくコレクションを変更する。
走査中の削除は it.remove() だけで行い、コレクションのメソッドで削除しないでください。
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("猫")) {
tasks.remove(task); // エラー!it.remove() を使うべき
}
}
GO TO FULL VERSION