倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (4) - intermediate operators part 1 -

Java の Stream インターフェースに定義されているメソッドを見ていくシリーズ(目次)。 今回からは java.util.stream.Stream インターフェースに定義されている intermediate operators を見ていきます。 「intermediate operator」は訳すと「中間演算子」ですかね? Map / Reduce 処理でいう Map 処理を行う演算子で、Stream オブジェクトを別の Stream オブジェクトへ変換します。 今回扱うメソッドは以下のもの:

// filter
Stream<T> filter(Predicate<? super T> predicate)

// map
<R> Stream<R> map(Function<? super T,? extends R> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

// flatMap
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
IntStream flatMapToInt(Function<? super T,? extends IntStream> mapper)
LongStream flatMapToLong(Function<? super T,? extends LongStream> mapper)
DoubleStream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)

// peak
Stream<T> peek(Consumer<? super T> consumer)

map() と flatMap() にはプリミティブ型(int, long, double)のストリームに変換するメソッドがありますが、使い方は同じなのでここでは割愛。 では、それぞれのメソッドをもう少し詳しく見ていきましょう。

filter() メソッド

filter() メソッドは、ある条件を満たす要素のみを通すフィルターの役割をします。 条件に合わない場合に要素を落とすので、一般に要素数は(同じか)減ります。 フィルターを通す条件は java.util.function.Predicate (述語)オブジェクトによって指定します。 ラムダ式で書く場合は boolean 値を返すようにすれば OK。 使い方はこんな感じ:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

stream().filter(s -> s.startsWith("J")).forEach(System.out::print);    // 「JavaJythonJRuby」と表示

簡単簡単。

map() メソッド

map() メソッドはストリームの各要素に一定の変換を施して、変換結果のオブジェクトを要素とするストリームを返します。 要素数は変換前後で同じです。

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

stream.map(s -> s.toUpperCase()).forEach(System.out::print);    // 「JAVAGROOVY・・・JRUBY」と表示
//stream.mapToInt(s -> s.length()).forEach(System.out::print);    // 「4657665」と表示

割愛するとか言いつつ、プリミティブ型に変換する mapToInt() を使ったサンプル書いちゃった。

flatMap() メソッド

flatMap() メソッドは、変換前の各要素からそれぞれ Stream オブジェクトを生成し、それらを繋げた Stream を返します。 通常、要素数は(同じか)増えます。

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

stream.flatMap(s -> stringToStream(s)).map(s -> s + " ").forEach(System.out::print);
    // 「J a v a G r o o v y S c a l a ・・・ J R u b y」と表示

/** 文字列を各文字からなる Stream に変換するメソッド。 もっとエレガントに行う方法あるかも。 */
public static Stream<String> stringToStream(String s){
    return IntStream.range(0, s.length()).mapToObj(i -> s.substring(i, i+1));

    /*StreamBuilder<String> builder = Stream.builder();

    for(int i = 0, n = s.length(); i < n; i++)
        builder.add(s.substring(i, i+1));

    return builder.build();*/
}

オブジェクトから Stream を生成するには Stream に定義されている static (ファクトリ)メソッドかコレクションの stream() メソッドを使えばいいかと思います。 配列を Stream にしたい場合は、何気に (static) ユーティリティ・メソッドが追加されている java.util.Arrays クラスを使えば何とかなるかと。

peek() メソッド

peek() メソッドは Stream オブジェクト自体には何も処理を施しませんが、各要素に対して副作用を起こす処理(というか副作用しか起こさない処理)を挟み込むメソッドです。 この副作用は java.util.function.Consumer オブジェクトによって指定します。 ラムダ式で指定する場合は返り値が void であれば OK。 Map / Reduce 処理中に中間状態を表示させたいといったデバッグなどに使うといいようです:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");

stream.peek(s -> System.out.println("[" + s + "]"))    // デバッグのための表示
        .flatMap(s -> stringToStream(s))
        .peek(s -> { assert s.length() == 1;} )    // テスト
        .map(s -> s + " ")
        .peek(s -> { assert s.length() == 2 && s.endsWith(" ");} )    // テスト
        .forEach(System.out::print);

こちらの記事でも書いたように、ラムダ式の中で assert 文を書こうとすると、ブロックにしないといけないもよう(Java 8 ea-b90)。 ちなみに、上記のサンプルを実行すると

[Java]
J a v a [Groovy]
G r o o v y [Scala]
S c a l a [Clojure]
C l o j u r e [Kotlin]
K o t l i n [Jython]
J y t h o n [JRuby]
J R u b y 

のように表示され、Stream オブジェクトに対するメソッド呼び出しが各メソッドごとに行われているわけではないことが分かります。 まぁ、Stream オブジェクト(や JVM の)実装によるんでしょうけど。

今回扱ったメソッドのうち、filter(), map(), flatMap() は大抵の関数型言語で同様のメソッドが定義されてます。 それだけ大事であり王道のメソッドってことですな。 次回は intermediate operator の続きの予定。

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

プログラミングClojure 第2版

プログラミングClojure 第2版