JavaのIteratorについて。
java.util.Iteratorは、(デザインパターンのひとつである)イテレーターパターンを実現するインターフェース。(JDK1.2から有る)
コレクション(ListやSet等の、値を複数持つクラス)等の要素をひとつずつ順次処理するのに使う。
import java.util.Iterator;
Iterator<String> i = 〜; while (i.hasNext()) { String s = i.next(); System.out.println(s); }
コレクションの要素を順次処理するという意味では、JDK1.5(Java5)で導入されたfor-each構文を使う方が分かりやすい。
Iterable<String> iterable = 〜; // コレクション for (String s : iterable) { System.out.println(s); }
また、JDK1.8(Java8)でコレクション(ListやSetの親であるIterable)にforEachメソッドが実装されたので、それとラムダ式を使う方法もある。
Iterable<String> iterable = 〜; // コレクション iterable.forEach(s -> System.out.println(s));
Iteratorインターフェースは、hasNextメソッドとnextメソッドを持っている。
hasNextメソッドで要素がまだ有るかどうか判定し、有る場合はnextメソッドでその値を取得する。
要素が存在する間、hasNextとnext呼び出しを繰り返す。
whileを使う例 | forを使う例 |
---|---|
Iterator<String> i = 〜; while (i.hasNext()) { String s = i.next(); System.out.println(s); } |
for (Iterator<String> i = 〜; i.hasNext();) { String s = i.next(); System.out.println(s); } |
個人的には、Iteratorの変数のスコープを限定する事が出来るので、for文を使う方が好み。
要素が無くなってからnextメソッドを呼び出すとNoSuchElementExceptionが発生する。
最初からIteratorに1件も要素が無い場合(空のIteratorの場合)、hasNextメソッドは常にfalseを返す。
一度Iteratorの要素が無くなると、そのIteratorで再度ループさせることは出来ない。
「最初に必ずhasNextメソッドを呼ばなければならない」といったルールは無いので、データが必ず有ると分かっている場合は、いきなりnextメソッドを呼び出す事も可能。
Set<String> set = Set.of("a", "b", "c"); assert !set.isEmpty(); Iterator<String> i = set.iterator(); String s = i.next();
JDK1.8(Java8)で、IteratorにforEachRemainingメソッドが追加された。
Iterator<String> iterator = 〜; iterator.forEachRemaining(s -> { System.out.println(s); });
要するにコレクションやStreamのforEachメソッドと同様にラムダ式(あるいはメソッド参照)を渡して全要素の処理が出来る。
メソッド名にわざわざ「Remaining」という単語が付いているのは、Iteratorを途中までループさせてからforEachRemainingメソッドを呼び出すと、残りの要素に対して処理を行う(Iteratorの最初からではない)為だと思う。
ミュータブル(要素の削除が可能)なコレクションから取得したIteratorの場合、Iteratorでループしながら元のコレクションの要素を削除することが出来る。
List<String> list = new ArrayList<>(List.of("a", "", "c")); System.out.println("before " + list); for (Iterator<String> i = list.iterator(); i.hasNext();) { String s = i.next(); if (s.isEmpty()) { i.remove(); } } System.out.println("after " + list);
↓実行例
before[a, , c] after [a, c]
i.next()を呼び出すことによってイテレーター内部では次の要素へ進んでいるような気がしてしまうが、
i.remove()を呼び出すと、直前のi.next()で取得していた要素が削除される。
ListやSet等のコレクション(の親であるIterableインターフェース)はiteratorメソッドを持っているので、それを呼び出せばIteratorを取得できる。
import java.util.Iterator; import java.util.List;
List<String> list = 〜; Iterator<String> i = list.iterator();
Mapにはiteratorメソッドは無いので、entrySet・keySet・valuesメソッドを呼び出してコレクションを取得し、そこからIteratorを取得する。
空のIteratorが欲しい場合はCollectionsを使用する。(JDK1.7(Java7)以降)
import java.util.Collections;
Iterator<String> iterator = Collections.emptyIterator();
JDK1.7より前の場合は、emptyListからIteratorを取得しておけばいいと思う。
Iterator<String> iterator = Collections.<String> emptyList().iterator();
Java8のStreamはiteratorメソッドを持っているので、StreamからIteratorを取得するのは簡単に出来る。
(Streamのiteratorメソッドは終端処理なので、2回以上Iteratorを取得することは出来ない)
IteratorからStreamを取得するメソッドは無いので、以下のようにして変換する。
import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport;
Iterator<String> iterator = 〜; Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); Stream<String> stream = StreamSupport.stream(spliterator, false);
IteratorをIterableに変換すれば、for-each構文で使用することが出来る。
IteratorインターフェースはIterableを継承しているわけではなく、Iterableを取得するメソッドを持っているわけでもない。
しかしIterableはIteratorを返すメソッドだけ実装すればいいので、独自のIterableを作ればよい。
Iterator<String> iterator = 〜; Iterable<String> iterable = new Iterable<>() { @Override public Iterator<String> iterator() { return iterator; } }; // for-each構文 for (String s : iterable) { System.out.println(s); }
ところで、Iterableインターフェースには@FunctionalInterfaceアノテーションは付いていないのだが、関数型インターフェースの条件を満たしているので、関数型インターフェースとして使用できる。
そこで、Iterableは以下のようにラムダ式を使って記述することが出来る。
Iterator<String> iterator = 〜; Iterable<String> iterable = () -> iterator; // for-each構文 for (String s : iterable) { System.out.println(s); }
ラムダ式を直接for文に書く場合は、Iterableへのキャストが必要となる。
(for文は「:
」の右側の式の部分をIterableとして推論してくれたりはしないから(参考:nagiseさんのツイート)、ラムダ式がどの関数型インターフェースなのかを明示する必要がある)
Iterator<String> iterator = 〜; // for-each構文 for (String s : (Iterable<String>) () -> iterator) { System.out.println(s); }
Iteratorを別クラスからメソッドで取得できる場合(例えばStreamからはiteratorメソッドで取得できる)は、メソッド参照を使って記述することも出来る。
(ListやSetといったコレクションは直接Iterableを継承しているので、変な小細工をしなくてもfor-each構文を使うことが出来る)
Stream<String> stream = 〜;
// Iterable<String> iterable = () -> stream.iterator();
Iterable<String> iterable = stream::iterator;
// for-each構文
for (String s : iterable) {
System.out.println(s);
}
Stream<String> stream = 〜;
// for-each構文
for (String s : (Iterable<String>) stream::iterator) {
System.out.println(s);
}