エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8 Stream APIの基本(3) - ストリーム生成

ストリームの生成方法についてまとめる。
ストリームを作成するAPIはコレクションなどの既存のクラスのいくつかに追加されており、Streamのstaticメソッドにもストリームを生成する手段が提供されている。 また、Streamの要素走査の中心的な役割である Spliteratorを作成することで、任意のデータ構造をストリームに対応させることもできる。

今回は、基本的な生成処理の内容を見ていく。

関数型インターフェースの種類

生成処理、および終端操作には様々な関数型インターフェースが登場するので、最初に関数型インターフェースについてまとめておく。
関数型インターフェースは、ラムダ式の戻り値となることができるインターフェースで、(デフォルトメソッドを除き)メソッドが1つだけ定義してあるインターフェースを指す。
インターフェースに @FunctionalInterface アノテーションを付与して関数型インターフェースであることを明言できるが、必ずしも付与する必要は無い。

以下にStreamで使用される主な関数型インターフェースをまとめておく。
StreamのAPIを読み解くには、Function, Consumer, Supplierの違いを理解することが重要だ。

関数型インターフェース 引数 戻り値 用途
Function<T,R> T R TからRへの変換を行う。mapなどで使用する
Consumer<T> T void 任意の副作用を実行する。forEach, peek などで使用する
Supplier<T> void T 値Tを生成する。Stream#generate, collect などで使用する
Predicate<T> T boolean Tから真偽値を判定する。filterなどで使用する
BiFunction<T,U,R> T,U R 2つの引数T,UからRへの変換・計算を行う。 collect 等で使用する。 これに限らず、Bixxxは2引数を取る
UnaryOperator<T> T T 単項演算。Functionで引数と戻りの型が同じ関数に相当する。 iterateなどで使用する
BinaryOperator<T> T,T T 2項演算。BiFunctionですべての引数と戻り値が同じ型の関数に相当する。 reduceなどで使用する

既存クラスに追加された生成メソッド

既存クラスに追加されたストリーム生成クラスには以下のようなものがある。 コレクションだけでなく、IOやFileにも提供されている。

クラス メソッド 戻り値 備考
Collection streamparallelStream Stream<T> paralellStreamは並列ストリームを生成する
Arrays stream Stream<T> 配列・可変長引数からストリーム生成する
BufferedReader lines Stream<String> 1行を1要素としたストリームを生成する
Files lines Stream<String> (上記と同じ)
Files list Stream<Path> ディレクトリを走査する
Files walk Stream<Path> 上記のlistに探索深度を設定できる
Files find Stream<Path> 上記のwalkに、検索条件をラムダ式で指定し、条件に一致するものを抽出する

Files#list, Files#walk, Files#find はディレクトリの木構造を簡単に辿れる機能だ。 従来であれば Files#walkFileTree とVisitorパターンを用いて実装する必要があったが、それに比べると簡単に実装が可能となった。

関連するものとして、IterableおよびMapには、forEachメソッドが追加されている。 よって、全要素を列挙するだけなら以下のように簡単に操作できる。

Arrays.asList(1,2,3,4,5).forEach(System.out::println);

Map<String, String> map = new HashMap<>();
map.put("aaa", "AAA");
map.put("bbb", "BBB");
map.forEach((k,v)-> System.out.println(k + ":" + v)); //map#forEachは、BiConsumer

Streamのstaticメソッド

次に、Stream(および関係するクラス)に用意されているStream生成手段をまとめる。
なお、メソッドによってはStreamとプリミティブStreamの両方に存在するものもある。

クラス メソッド 引数 備考
Stream of T, T... 可変長引数・配列から生成する
Stream empty 要素0のストリームを生成する
Stream concat Stream<T>, Stream<T> 1番目のStreamの最後の要素の後に2番目のStreamを連結する
Stream generate Supplier<T> 主に固定値無限ストリームを生成する
Stream iterate T, UnaryOperator<T> 無限ストリームを生成する
IntStream range int, int 第一引数から第二引数までのint値のストリームを生成する
LongStream range long, long 第一引数から第二引数までのlong値のストリームを生成する
Stream builder Stream.Builderを構築する(後述)

無限ストリームの構築

無限ストリームの構築手段にはgenerate, iterate の2つがある。
generateは要素の取得のたびにSupplierで指定した関数の戻り値が取得される。 Supplierで状態を保持する関数(外部変数の参照、乱数、現在時刻の取得などが考えられる)を実装しない限り、通常は常に固定値を生成する用途で使用する。

Stream.generate(() -> 1); // 常に1を返すストリーム

iterateは、初回の取得は第一引数、以降の取得は第二引数のUnaryOperatorに第一引数または前回の演算の結果を再帰的に適用した値を取得する。
下記のコードを評価すると、初期値1, 以降 n + 1(1 + 1 = 2, 2 + 1 = 3, 3 + 1 = 4,,,,)のストリームが生成される。

Stream.iterate(1, n -> n + 1); //1,2,3,4,5,,,,を生成する。

以下のような無限ループに対応すると考えればよい。

for (int n = 1; ; n++) {}

Stream.Builder

Stream.Builderは汎用的なストリームの構築手順である。add,acceptで順次要素を追加していき、最終的にbuildメソッドStreamを構築する。 buildメソッドを実行した後は、要素を追加できない。
ListSetを用いても同様な処理は可能であるが、 Stream.Builderを用いた方が効率が良い。

以下にStream.Builderを使って、文字列を1文字ずつのStreamに変換する例を示す。
ただし、あくまで使用例であり、サロゲートペアに対応していない。

public static Stream<String> each(String str) {
    Stream.Builder<String> sb = Stream.builder();
    for (int i = 0; i < str.length(); i++) {
        sb.add(str.charAt(i) + "");
    }
    return sb.build();
}

public static void main(String[] args) {
    String s = each("あいうえお").collect(Collectors.joining("-"));
    System.out.println(s); // あ-い-う-え-お
}

余談だが、意外にも同様の処理を行う機能は既存のクラスには無い。 代わりに、String#charsString#codePoints というメソッドがあり、前者は文字、後者はサロゲートペアに対応した文字の整数値のストリーム(IntStream)を構築するメソッドである。 サロゲートペアを考慮すると単純に Characterのストリームとする事ができないため、このような対応になったと思われる。 上記の1文字ごとの文字列への分割を、サロゲートペアを考慮して行うと以下のようになる。

public static Stream<String> eachSurrogatePair(String str) {
   return str.codePoints().boxed().map(i -> String.valueOf(Character.toChars(i)));
}

Stream.Builderは小さいデータからストリームを構築するのには有用だが、上記のように構築の際にストリームに入れるデータをループ等で全て取得する必要がある。 そのため、Stream.Builderで大量データのストリーム構築を行う場合は非効率であり、IO等のデータから構築する場合も構築段階でIOの全てのデータを読み取ってしまうので、Streamの特徴である遅延実行の性質を生かすことができない。

任意のデータから効率的にストリームを構築するためには、Stream の内部仕様であるSpliteratorを押さえておく必要がある。

まとめ

基本的な生成処理についてまとめた。
次回はStreamの内部で扱われるSpliteratorについて解説する。

[前多 賢太郎]