終了条件をつけた 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 に入れておくなどの妥協案をとらず、あくまでもストリーム処理したい場合は、以下のように行う必要があるっぽい。

  1. 終了条件にマッチしたら hasNext() が false になる Iterator を作成する
  2. Splitrators.spliteratorUnknownSize()Iterator から Splitrator を作成する
  3. 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