終了条件をつけた Stream を作成する
たとえば正規表現で文字列をスキャンし、マッチした部分文字列からなる Stream を作成したいとする。この Stream は、マッチしなくなったら終了する必要があるが、どうするか?
Stream を簡単に作成できる Stream#generate() だと無限のストリームしか作れず、「条件にマッチしたら終了させる」ことができない。以下のようにすると、当然のようにプログラムが終了しない。
// 文字列に含まれる数値の合計を表示 public static void main(String[] args) throws Exception { String s = "1 2 3 4 5 6 7 8 9 10"; int sum = scannedStream(Pattern.compile("\\d+"), s) .mapToInt(Integer::valueOf) .reduce(0, Integer::sum); // "sum: 55" と表示させたい System.out.println("sum: " + sum); } /** * pattern に一致した部分文字列のストリームを返す */ private static Stream<String> scannedStream(Pattern pattern, String target) { Matcher m = pattern.matcher(target); return Stream.generate(() -> m.find() ? m.group() : null).filter(s -> s != null); }
Ruby の take_while のような機能が欲しいところだけど、Java の Stream には無い。
ここで、いったん全部をマッチさせて List に入れておくなどの妥協案をとらず、あくまでもストリーム処理したい場合は、以下のように行う必要があるっぽい。
- 終了条件にマッチしたら hasNext() が false になる Iterator を作成する
- Splitrators.spliteratorUnknownSize() で Iterator から Splitrator を作成する
- StreamSupport.stream() で Spliterator から Stream を作成する
上の scannedStream の修正版は、以下のようになる。
private static Stream<String> scannedStream(Pattern pattern, String target) { Matcher m = pattern.matcher(target); Iterator<String> it = new Iterator<String>() { @Override public boolean hasNext() { return m.find(); } @Override public String next() { return m.group(); } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED), false); }
この文字列マッチの場合はそもそも並列実行できないので、あまり Stream の有り難みはないけど。
参考情報
JDK8の java.util.stream のパッケージドキュメントに大体書いてある。
https://docs.oracle.com/javase/jp/8/api/java/util/stream/package-summary.html
あと、この辺が詳しい。
http://enterprisegeeks.hatenablog.com/archive/category/Java8