無限Streamに終了条件を設定する
例えばDOMのようなツリー構造があったとして、あるノードを渡された時に、ルートからそのノードまでの名前を連結してパス文字列を作成する、というプログラムを考えます。
Java7でも動くように、副作用ループを使った書き方だとこんな感じ。
public static String getTreePath(Node node, String delimiter) { LinkedList<String> nodePath = new LinkedList<>(); for (Node n = node; n != null; n = n.getParent()) { //根でgetParentするとnullが帰るものとする nodePath.add(0, n.getName()); } return delimiter + String.join(delimiter, nodePath); }
んで、これをJava8のStream APIで書くとどう書けるのか考えてみて、最初に書いたのがこれ。
public static String getTreePath2(Node node, String delimiter) { List<String> names = Stream.iterate(node, Node::getParent) .filter(n -> n != null) .map(o -> o.getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
半分くらい書いたところで、あれ?って思ったんですが、案の定これは NullPointerException で落ちました。Stream.iterate に渡した関数(Node::getParemt)を連鎖的に評価させてその結果をリスト化するわけですが、getParent が null を返しても、そこで評価を止めることができないわけですね。
API Document とかいろいろ漁って調べてみましたが、単純に Stream.iterate や Stream.generate で生成した無限 Stream は、limitで決まった数で区切るか、zip で有限 Stream と結合するか、短絡評価をする終端操作で区切るしかなさそうです。「指定した条件を満たした最初の要素より前のものだけ抽出」のような中間操作ないし終端操作はない模様。
こういう場合は、 Stream の作成のしかたからカスタマイズする必要があって、API Documentによると、「最も単純だが低パフォーマンスな」作成方法は、Iterator の実装クラスを作って、そのインスタンスから Splitterator を作ることとありました。なるほど、Iterator なら生成関数と終了条件関数をセットで実装できますね。
で書いてみたのがこちら。
public static String getTreePath3(Node node, String delimiter) { Iterator<Node> iter = new Iterator<Node>() { private Node n = node; @Override public boolean hasNext() { return (n != null); } @Override public Node next() { Node pre = n; n = pre != null ? pre.getParent() : null; return pre; } }; List<String> names = StreamSupport.stream( Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), false) .map(o -> o.getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
長ぇ。
最初のループを使った書き方とくらべても断然めんどくさいですね。
Stream を作るところを、別のメソッドに切り出して、軽く汎用的にしてみます。引数としては、 Stream.iterate の引数に終了条件 term を加えた形ですね。
private static <T> Stream<T> iterateLimited(T seed, UnaryOperator<T> generator, Predicate<T> term) { Iterator<T> iter = new Iterator<T>() { T current = seed; @Override public boolean hasNext() { return !term.test(current); } @Override public T next() { T pre = current; current = generator.apply(pre); return pre; } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), false); }
これを使うと、メインの処理はこんなふうに書けます。(ついでに Optional 使って null を隠蔽してみました)
public static String getTreePath4(Node node, String delimiter) { List<String> names = iterateLimited( Optional.ofNullable(node), o -> o.map(n -> n.getParent()), o -> !o.isPresent()) .filter(o -> o.isPresent()) .map(o -> o.get().getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
だいぶ「らしく」なったような気がします。
… iterateLimited みたいな関数は、標準で用意してもらいたいものですが…
(2017/12/06追記)
Java9では簡単に書けるようになりました。
noisyspot.hatenablog.com