前書き
イテレータのラッパークラスを作る際、hasNextの実装に困ることが多い。
ただラップするだけなら内部のイテレータに委譲すれば良いが、都合上そのような戦略を取れない場合だ。
次の要素を覗き見れれば便利なのに... というわけで、作ってみる。
本題
次のような動作をするクラス、PeekableIteratorを作ってみた。
var src = java.util.List.of(3, 1, 4);
var peekIt = new PeekableIterator<>(src);
while(peekIt.hasNext()) {
System.out.printf("次の要素は %d です。\n", peekIt.peek());
System.out.printf("もう一度言います。 %d です。\n", peekIt.peek());
System.out.printf("ほら、本当に %d だったでしょ?\n", peekIt.next());
System.out.println();
}
System.out.println("おしまい");
実行結果
次の要素は 3 です。
もう一度言います。 3 です。
ほら、本当に 3 だったでしょ?
次の要素は 1 です。
もう一度言います。 1 です。
ほら、本当に 1 だったでしょ?
次の要素は 4 です。
もう一度言います。 4 です。
ほら、本当に 4 だったでしょ?
おしまい
実装
メソッドの構成
コンストラクタと本命のメソッドpeek()だけを加えたシンプルな構成である。
hasNext()が偽のとき、peek()は例外NoSuch~を投げる。これはIterator#next()の仕様をなぞったものである。
import java.util.Iterator;
import java.util.NoSuchElementException;
public class PeekableIterator<T> implements Iterator<T> {
public PeekableIterator(Iterable<T> iterable) { ... }
public PeekableIterator(Iterator<T> it) { ... }
public T peek() throws NoSuchElementException { ... }
@Override
public T next() throws NoSuchElementException { ... }
@Override
public boolean hasNext() { ... }
}
フィールドの構成
構成は次のとおりである。
it.next()がnullを返す可能性も考慮し、nextElemとhasNextをセットで取り扱っている。
private final Iterator<T> it; // 大元のイテレータ
private T nextElem; // 次の要素
private boolean hasNext = false; // 次の要素が存在するか?
メソッドの実装
コンストラクタではさっそく覗き見をし、次の要素をフィールドに保持している。
public PeekableIterator(Iterable<T> iterable) {
this(iterable.iterator());
}
public PeekableIterator(Iterator<T> it) {
this.it = it;
if(it.hasNext()) {
nextElem = it.next();
hasNext = true;
}
}
メソッドhasNext()は、結局ただのゲッターである。
next()はnextElemを返すだけだが、次の要素を覗き見する処理が必要だ。
@Override
public boolean hasNext() {
return hasNext;
}
@Override
public T next() throws NoSuchElementException {
if(!hasNext()) {
throw new NoSuchElementException();
}
final T ret = nextElem;
if(it.hasNext()) { nextElem = it.next(); }
else { hasNext = false; }
return ret;
}
本命peek()の実装も簡単だ。ただしnextElemが無効であるかチェックしなければならない。
public T peek() throws NoSuchElementException {
if(!hasNext()) {
throw new NoSuchElementException();
}
return nextElem;
}
全体のコード
import java.util.Iterator;
import java.util.NoSuchElementException;
public class PeekableIterator<T> implements Iterator<T> {
//
private final Iterator<T> it;
private T nextElem;
private boolean hasNext = false;
public PeekableIterator(Iterable<T> iterable) {
this(iterable.iterator());
}
public PeekableIterator(Iterator<T> it) {
this.it = it;
if (it.hasNext()) {
nextElem = it.next();
hasNext = true;
}
}
//
public T peek() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
return nextElem;
}
//
@Override
public boolean hasNext() {
return hasNext;
}
@Override
public T next() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
final T ret = nextElem;
if (it.hasNext()) {
nextElem = it.next();
} else {
hasNext = false;
}
return ret;
}
}
利用上の問題
イテレータを独占していないことに起因する問題
コンストラクタでイテレータを受け取ったとき、特に複製などはしていない。
そのため、呼び出し元でイテレータを直接操作してしまうと要素がスキップされたように見えてしまう。
var it = java.util.List.of(3, 1, 4).iterator();
var peekIt = new itertools.PeekableIterator<>(it);
it.next();
while(peekIt.hasNext()) {
System.out.printf("次の要素は %d です。\n", peekIt.peek());
System.out.printf("もう一度言います。 %d です。\n", peekIt.peek());
System.out.printf("ほら、本当に %d だったでしょ?\n", peekIt.next());
System.out.println();
}
System.out.println("おしまい");
実行結果
次の要素は 3 です。
もう一度言います。 3 です。
ほら、本当に 3 だったでしょ?
次の要素は 4 です。
もう一度言います。 4 です。
ほら、本当に 4 だったでしょ?
おしまい
しかしイテレータを完全に独立させるのは、どうやら不可能そうである。
- イテレータはいわば『状態』であり、それがそのまま複製できるかどうかは提供側に依存する。
- 全要素を読み出せば幾らでもイテレータが作れるが、要素の取得を遅延できるメリットが消える。
- そもそも全要素を読み出した時点で、元のイテレータが枯れる。
- そもそも無限に要素を返すイテレータはどうするのだ。
イテレータに『利用不可』属性を付与できるような仕組みがあると良いのだが...
端点でpeek()した際に例外が発生する問題
仕様どおりではあるけれど。
var src = java.util.List.of(3, 1);
var peekIt = new PeekableIterator<>(src);
while(peekIt.hasNext()) {
System.out.printf("今の要素は %d です。\n", peekIt.next());
System.out.printf("次の要素は %d です。お楽しみに。\n", peekIt.peek());
System.out.println();
}
System.out.println("おしまい");
実行結果
今の要素は 3 です。
次の要素は 1 です。お楽しみに。
今の要素は 1 です。
Exception in thread "main" java.util.NoSuchElementException
at ...
既存のライブラリ
本プログラムのインターフェースを決定する際には、Pythonのmore_itertools.peekableを参考にした。
しかし改めて調べてみると、Javaでも同様のクラスが提供されているらしい。
端点をどのように処理しているのか着目しながら、実際に使ってみることとする。
Apache Commons: Class PeekingIterator<E>
peekingIterator() でもインスタンスを得られるが、コンストラクタとの違いは不明。1
目的に応じてelement()とpeek()を使い分けられる。地味に便利。
var src = java.util.List.of(3, 1);
var peekIt = new PeekingIterator<>(src.iterator());
while(peekIt.hasNext()) {
System.out.printf("今の要素は %d です。\n", peekIt.next());
System.out.printf("次の要素は %d です。お楽しみに。(peek)\n", peekIt.peek());
System.out.printf("次の要素は %d です。お楽しみに。(element)\n", peekIt.element());
System.out.println();
}
System.out.println("おしまい");
実行結果
今の要素は 3 です。
次の要素は 1 です。お楽しみに。(peek)
次の要素は 1 です。お楽しみに。(element)
今の要素は 1 です。
次の要素は null です。お楽しみに。(peek)
Exception in thread "main" java.util.NoSuchElementException
at ...
Guava: Interface PeekingIterator<E>
Iterators.peekingIteratorを介してインスタンスを受け取ることになる。
こちらにはelement()メソッドは無く、終端のpeek()は例外を吐いて終わるようだ。
var src = java.util.List.of(3, 1);
var peekIt = Iterators.peekingIterator(src.iterator());
while(peekIt.hasNext()) {
System.out.printf("今の要素は %d です。\n", peekIt.next());
System.out.printf("次の要素は %d です。お楽しみに。\n", peekIt.peek());
System.out.println();
}
System.out.println("おしまい");
今の要素は 3 です。
次の要素は 1 です。お楽しみに。
今の要素は 1 です。
Exception in thread "main" java.util.NoSuchElementException
at ...
後書き
おしまい。
-
そんなに真面目に調べていないので、案外ググったらすぐ出るかも。 ↩