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 |
stream ・parallelStream |
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
メソッドを実行した後は、要素を追加できない。
List
や Set
を用いても同様な処理は可能であるが、 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#chars
、String#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
について解説する。
[前多 賢太郎]