Stream APIの始め方

この記事は、Java Advent Calendar 2013の2日目として書かれたものです。

Java SE 8では、Stream APIという新しいAPIが導入されます。Stream APIは、何らかの要素の集まり(配列やコレクション、あるいはテキストファイルを読み取るときの一行一行など)に対するさまざまな操作を抽象化した、とても便利なAPIです。

Stream APIは、Java SE 8の新機能としても特に注目されているものの1つでしょう。類似の仕組みがC#やScalaやRubyなど他の言語でも広がっており、Javaにもついに、と待ち望んでいる方も多いでしょう。

ところが、Streamインターフェースが持つ様々な便利メソッド(filter、map、flatMap、collectなど)は、あちこちでよく紹介される一方で、次の2点は、あまり注目されていないように思いました(ぼくが見た限りで)。

  • 既存のさまざまなデータの集まりから、Streamのインスタンスをどうやって作るか。
  • Javaの標準APIでどのようなところでStreamが使われているか。

そこで本記事では、Java SE 8の標準APIに用意されている、Streamを作り出すメソッドをほぼすべて、ざっと紹介してみたいと思います。ただしmapのような、Streamから別のStreamを作るインスタンスメソッドは除きます。

Stream APIを試してみるにあたって、11月にリリースされた開発者プレビュー版(b117)を用いました。Java SE 8は2014年3月(あと4ヶ月!)のリリースに向けてAPIは固まっているので、ここで紹介したものがなくなることはない…はず…です。

CollectionからStreamを作る

ListやSetなどのインターフェースの親にあたるCollectionには、Streamを作るためのデフォルトメソッドstream()とparallelStream()が追加されました。

List<String> list = Arrays.asList("");
Stream<String> stream = list.stream();

配列の全要素を含むStreamを作る

配列には、Streamを作り出すためのメソッドは追加されませんでした。その代わりに、従来からあるユーティリティクラスであるArraysに、配列をstreamへ変換するユーティリティメソッドが追加されました。

Object[] array = {"string", 1, new ArrayList<Integer>()};
Stream<Object> many2 = Arrays.stream(array);

また、Streamクラスにも、受け取った配列からStreamを作り出すstaticメソッドが用意されています。

Stream<String> many = Stream.of("a", "b", "c", "d", "e");

なお現時点(b117)の実装では、Stream.of(T...)はArrays.stream(T[])を呼び出しているだけなので、両者が生成するStreamに違いはありません。強いて違いをあげれば、Stream.of(T...)は可変長引数を取るので、Arrays.stream(new Hoge[]{a,, b, c })に比べ、Stream.of(a,b,c)などと短く書けるくらいでしょうか。

またStream APIでは、int、long、doubleの3つのプリミティブは特別扱いされています。これら3プリミティブの配列から、それぞれに対応するStreamを作り出すユーティリティメソッドも用意されています。

int[] ints = {1, 2, 3, 4, 5};
IntStream stream1 = IntStream.of(ints);
IntStream stream2 = IntStream.of(1, 2, 3, 4, 5); // 可変長引数
IntStream stream3 = Arrays.of(ints);

longやdoubleも同様です。

配列の一部を含むStreamを作る

Streamは、ある範囲を切り出すメソッドlimit(long)やskip(long)を持っています。これらを使えば、配列から作ったStreamの任意の範囲を切り出せます。この方法を使うと、実際の配列のサイズより大きくlimit(256)あるいはskip(256)などとした場合、結果のStreamは空になります。

String[] array = {"1", "2", "3", "4", "5", "6", "7"};
Stream<String> partial = Arrays.stream(array).limit(5).skip(2);
partial.forEach(System.out::println);

ここでは、配列のインデックスが2~4までの範囲を切り出しています。実行すると、次のように出力されるはずです。

3
4
5

ですが、配列のある範囲しか必要でないことが分かっている場合は、より効率的な方法があります。この方法だと、実際の配列のサイズより大きなインデックスを指定してしまうと、ArrayIndexOutOfBoundsExceptionが投げられます。

String[] array = {"10", "20", "30", "40", "50", "60", "70"};
Stream<String> partial = Arrays.stream(array, 2, 5); 
partial.forEach(System.out::println);

結果は次のようになります。array[5]すなわち"60"が結果に含まれないことに注意してください。

30
40
50

int、long、doubleのプリミティブ配列にも、同じものが用意されています。

int[] a = {10, 20, 30, 40, 50, 60, 70};
IntStream partial = Arrays.stream(a, 2, 5);
partial.forEach(System.out::println); // 30, 40, 50

シングルトンStream(要素が1つだけのStream)を作る

要素が1つしかない配列やコレクションからStreamを作ればよい…でしょうか? もちろん、その方法でもできることはできます。ただそうしてしまうと、元になった配列やコレクションに加えられた変更が、Streamにも伝わってしまいます(それを望むなら別ですが)。そこで配列やコレクションに依存しないシングルトンStreamを作るには、次の方法があります。

Stream<String> singleton1 = Stream.of("one");
プリミティブだと、
IntStream singleton2 = IntStream.of(42);
LongStream singleton3 = LongStream.of(42L);
DoubleStream singleton4 = DoubleStream.of(0.123);

文字列からStreamを作る

まず、文字列(CharSequence…String、StringBuilder)から1文字ずつ取り出す方法です。1文字ずつ加工したり取捨選択したりしたいときに使えますね。

IntStream chars = "abcde".chars();

結果のStreamの型がStream<Character>あるいはCharStreamでないことに注意しましょう。プリミティブのcharには、CharStreamのような特別なStreamが用意されていません。

しかし、Stream<Character>でもなく、その代わりにIntStreamが使わてます。これはなぜでしょうか? Unicodeで扱える補助文字はcharより範囲が広いため、intじゃないと表現できないからだそうです(不勉強)。

続いて、1行ずつ分解したStreamを作る方法です。

Stream<String> lines = new BufferedReader(new StringReader("abc\ndef\nghi")).lines();
lines.forEach(System.out::println); // abc, def, ghi

このBufferedReaderを使った方法だと、実行環境の改行文字に依存してしまうので、注意が必要です。

最後に、任意のパターンで文字列を分割する方法です。もちろん正規表現を使います。Patternクラスは、もともと、任意の文字列をString[]に分割するsplit(CharSequence)メソッドを持っていました。新しく追加されたメソッドを使うと、スムーズにStreamを作ることができます。

Pattern pattern = Pattern.compile("\\d{2}");
Stream<String> s = pattern.splitAsStream("a12b34c56d");
s.forEach(System.out::println); // a, b, c, d

またこのように正規表現を使った方法であれば、Pattern.compile("\\r?"\\n).splitAsStream(input)などとすることで、どのような改行文字であっても1行ごとに分割できるでしょう。

文字ストリームからStreamを作る

文字の集まりといえば、Java SE 8以前からある文字ストリーム(Streamクラスと紛らわしいですが)もそのひとつです。BufferedReaderを使うことで、任意の文字ストリームの1行を送り出すStreamを作ることができます。

例えば、任意のWebページを1行ずつのStreamにしてみましょう。

URL example = new URL("http://www.example.com/");
try (InputStream in = example.openStream();) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    Stream<String> lines = reader.lines();
    lines.forEach(System.out::println);
}

テキストファイルからStreamを作る

上の例でもあげたBufferedReaderを使うことで、テキストファイルからStreamを作ってみましょう。

BufferedReader reader = Files.newBufferedReader(Paths.get("hoge.txt"), Charset.defaultCharset());
Stream<String> lines = reader.lines();

このパターンは多用されるのか、さらに短く書けるユーティリティメソッドまで用意されています。

Stream<String> s = Files.lines(Paths.get("hoge.txt"), Charset.defaultCharset());

ファイルツリーからStreamを作る

あるディレクトリの階層をたどって、ファイルやディレクトリを1つ1つ舐めていく操作は、よくあります。そうした用途のために、ファイルツリーからStreamを作り出す方法も用意されています。

まずは単純な、ある親ディレクトリの直下にあるファイルや子ディレクトリを含むStreamです。親ディレクトリそのものは含みません。また、子ディレクトリのさらに子ディレクトリを再帰的にたどることもありません。たどる順序は、ファイル名の自然順のようです。

Path root = Paths.get("/usr/foo/picture/サナカン");
Stream<Path> tree = Files.list(root);

次に、子ディレクトリを再帰的にたどっていく(walk…うろつく)方法です。

Path root = Paths.get("/usr/foo/picture/サナカン");
Stream<Path> tree = Files.walk(root);

このとき、探索の深さは無制限(実際はInteger.MAX_VALUEまで)で、深さ優先となります。たどる順序はファイル名の自然順序です。ルートに指定したディレクトリ自身も含まれることに注意してください。実際のファイルツリーを例に列挙順序を図解してみると、次のようになります。

深さ列挙順序
0123
サナカン1
1.jpg2
DirA3
1.jpg4
DirA15
1.jpg6
ZZZ.txt7
DirC8
DirC19
1.jpg10
ZZZ.txt11

続いて、深さに制限をかける方法です。walkメソッドの第2引数に深さを指定します。親要素の深さは、基準となる0です。

Path root = Paths.get("/usr/foo/picture/サナカン");
Stream<Path> tree = Files.walk(root, 2);

これを図解してみましょう。Streamには、深さ2までのファイルとディレクトリが含まれます。深さ3のファイルやディレクトリは、飛ばされます。

深さ列挙順序
0123
サナカン1
1.jpg2
DirA3
1.jpg4
DirA15
1.jpg
ZZZ.txt6
DirC7
DirC18
1.jpg
ZZZ.txt9

こうしてファイルツリーのStreamを作り、あとはfilterで絞り込んでいけば基本は十分そうです。

さらなる効率を求めるために、さらにユーティリティメソッドfindが用意されています。findは、ファイルのパス(Path)と属性(BasicFileAttributes)の2引数からなる述語関数BiPredicateを受け取り、その述語に合致するものだけをStreamに含めます。

Path root = Paths.get("/usr/foo/picture/サナカン");
Stream<Path> tree = Files.find(root, Integer.MAX_VALUE, (path, attr) -> attr.isRegularFile());

この例では、深さは無制限に、普通のファイルのみを含めています。図解すると、次のとおりです。

深さ列挙順序
0123
サナカン
1.jpg1
DirA
1.jpg2
DirA1
1.jpg3
ZZZ.txt4
DirC
DirC1
1.jpg5
ZZZ.txt6

空のStreamを作る

Stream APIを利用していくとき、空のStreamを使いたくなることは出てくると思います。例えば、StreamのflatMapメソッドに渡すFunctionが条件によっては空のStreamを返すとき、です。そうしたところで使える空のStreamは、次のように作ることができます。

Stream<String> s1 = Stream.empty();

例によって、プリミティブ(int、long、doubleのみ)版もあります。

IntStream s2 = IntStream.empty();
LongStream s3 = LongStream.empty();
DoubleStream s4 = DoubleStream.empty();

無限に値を供給し続けるStream

Stream APIでは、Integer.MAX_VALUE以上の長さ、もっといえば無限に続くStreamを作ることもできます。Streamに無限に値を供給するには、値を次々と生成する関数型インターフェースを使います。

まず初めに、毎回その場その場で値を生成(generate)し続けるStreamです。generateには、関数型インターフェースSupplierを使います。Supplierは、何らかの引数を受け取って結果を返す関数型インターフェースFunctionなどとは違い、引数なしで値を供給します。

Stream<String> infiniteMuda = Stream.generate(() -> "無駄"));
List<String> fiveMuda = infiniteMuda.limit(5).collect(Collectors.toList());
// 無駄, 無駄, 無駄, 無駄, 無駄

またgenerateには、プリミティブ版もあります。

DoubleStream infiniteRandomDouble = DoubleStream.generate(Math::random);

次に、前回の値をもとに次の値を作っていくことをひたすら反復(iterate)し続けるStreamです。iterateには、関数型インターフェースUnaryOperatorを使います。UnaryOperatorは、前回の値を引数として受け取って、引数と同じ型の結果を返します。

Stream<String> iteratedMuda = Stream.iterate("無駄", prev -> prev.concat("!"));
List<String> collect = iteratedMuda.limit(5).collect(Collectors.toList());
// 無駄, 無駄!, 無駄!!, 無駄!!!, 無駄!!!!

iterateの最初の引数は、Streamの最初の要素として使われる、初期値です。初期値が必要になるのは、UnaryOperatorの引数には前回の値が渡されますが、初回のみは前回の値が存在しないためです。

また例によって、プリミティブ版もあります。

IntStream iterated = IntStream.iterate(1, prev -> prev + 2);
int[] ints = iterated.limit(5).toArray();
// 1, 3, 5, 7, 9

これらの方法は、例えば、テストデータなどをランダムに生成し続けるStreamなど、様々な応用ができると思います。

ある範囲の数値からなるStreamを作る

18から40までなど、何らかの範囲に含まれる数値を列挙したいことは、よくあるのではないでしょうか。Stream APIでも、そのような使いどころはサポートされています。

// 2, 3, 4, 5, 6, 7, 8, 9
IntStream range1 = IntStream.range(2, 10);
IntStream range2 = IntStream.rangeClosed(2, 9);
LongStream range3 = IntStream.range(2, 10);
LongStream range4 = IntStream.rangeClosed(2, 9);

標準では、上であげた4つ…すなわち、intかlongのどちらかで、数値が1ずつ増加していくものしかありません。それ以外のユースケースは、先に紹介したiterateなどを使って実現できそうです。例えば、ステップが1以外の場合は、次のように書けるでしょう。

// 1, 3, 5, 7
IntStream.iterate(1, i -> i + 2).limit(4);

// 2, 1, 0, -1, -2, -3 
IntStream.iterate(2, i -> i - 1).limit(6);

DoubleStreamにはrangeメソッドやrangeCloseメソッドは用意されていませんが、doubleの範囲もiterateを使えば実現できそうです。ただし、少数の精度には注意してください。

// 0.0, 1.0, 2.0, 3.0
DoubleStream.iterate(0, d -> d + 1).limit(4);

ランダムな数字のStreamを作る

ランダムな数字が続く無限Streamを作る方法が、Randomクラスのインスタンスメソッドとして定義されています。RandomのサブクラスであるSecureRandomやThreadLocalRandomでも使えます。

// ランダムなintが無限に続くIntStreamを使い、ランダムなintの配列を生成。
Random random = new Random();
IntStream randomInts = random.ints();
int[] array = randomInts.limit(10).toArray();

// Streamに含まれる要素数をあらかじめ指定。
IntStream random10 = random.ints(10);
int[] array2 = random10.toArray();

ランダムに生成されるintの範囲を指定することもできます。

// 指定した範囲内のintが無限に続くIntStream、ただし大きい方の数値は含まれない。以下例だと1~4まで。
IntStream ranged = random.ints(1, 5);
int[] array = ranged.limit(10).toArray();

// Streamに含まれる要素数をあらかじめ指定。
IntStream ranged2 = random.ints(10, 1, 5);
int[] array2 = randomInts.toArray();

しつこいようですが、intsだけでなく、longsやdoublesもあります。

2つのStreamを結合した新たなStreamを作る

なぜかStreamのインスタンスメソッドではなく、クラスのstaticメソッドとして定義されています。

Stream<String> a = Stream.of("星白閑", "科戸瀬イザナ", "緑川纈");
Stream<String> b = Stream.of("仄姉妹", "サマリ・イッタン");
Stream<String> concated = Stream.concat(a, b);
concated.forEach(System.out::println); // 星白閑, 科戸瀬イザナ, 緑川纈, 仄姉妹, サマリ・イッタン

プリミティブ用のStreamにも、それぞれのconcatが定義されています。

IntStream concated2 = IntStream.concat(IntStream.of(1,2,3), IntStream.of(4,5));
concated2.forEach(System.out::println); // 1, 2, 3, 4, 5

ビット配列からStreamを作る

BitSetクラスは、インデックスで効率よくアクセスできるビット(true=1、false=0)の配列のようなものです。他の例にでてきたクラスに比べると、あまりなじみのないクラスかもしれません。ビット配列という性質からboolean[]を連想されるかもしれませんが、BitSetはboolean[]よりもメモリ効率がよくなる、サイズが動的に可変であるといった特徴を持ちます。

BitSetに新たに追加されたstream()メソッドは、セットされている(1になっている)ビットのインデックスを小さい順に並べたIntStreamを生成します。

BitSet bitSet = new BitSet();
bitSet.set(2, 4);
bitSet.set(7);
int[] indexed = bitSet.stream().toArray(); // 2, 3, 7

従来、BitSetのインスタンスで、セットされている(1になっている)ビットのインデックスを調べあげるのは面倒でした。stream()を使うことで、セットされたビットをまとめて扱うのが簡単になりました。

Stream.Builderを使ってStreamを作る

Stream.Builder(Streamインターフェースにネストして定義されています)は、その名の通り、Streamを作り出すビルダーを表すインターフェースです。immutable(変更不可能)なStringに対してmutable(変更可能)なStringBuilderが存在するのと同じように、immutableなStreamに対してmutableなStream.Builderが用意されています。

Streamに含める値が、次の例のように複雑に変わる場合などに使えそうです。実際にはあまりお目にかかりたくない感じがしますが、StringBuilderを使うときがしばしばあるように、Stream.Builderもきっと使う必要がどこかで出てくるのでしょう。

Stream.Builder<String> builder = Stream.builder();
if (someCondition()) {
    builder.accept("foo");
} else {
    builder.add("bar").add("buz");
}
if (anotherCondition()) {
    for (String s : new String[]{"a", "b", "c"}) {
        builder.accept(s);
    }
} else {
    builder.accept("x");
}
Stream<String> stream = builder.build();

なお、ArrayListを一時保存領域として使い、list.stream()すれば同じことはできます。Stream.Builderのjavadocによると、Stream.Builderを使ったほうが、ArrayListから値を作るよりも効率が良いそうです。ぼくの環境でいくつかテストしたところ、ArrayListを使うよりも数%から最大で7割ほど高速化しました。何度か遅くなることもあったので一概には言えませんが、コードの分かりやすさ(ビルドする!)とパフォーマンスの両面から、Builderを使ったほうが良さそうです。

またしつこいようですが、プリミティブStreamにもIntStream.builder()などが用意されています。

任意のIterableからStreamを作る

Collectionの親にあたるIterableには、Streamを直接作り出すためのメソッドがありません。代わりに、Spliteratorを作って、そこからStreamを作る方法が用意されています。

Iterable<String> iter = Arrays.asList("");
Spliterator<String> spliterator = iter.spliterator();
boolean parallel = false;
Stream<String> StreamSupport.stream(spliterator, parallel);

ここで使ったStreamSupportクラスは、Stream APIに含まれる1クラスです。Streamを生成したり、操作するためのユーティリティメソッドを含んでいます。いわばStreamにとっての、CollectionsクラスやArraysクラスですかね。

StreamSupport.streamの2番目の引数は、そのStreamが並行処理できるか否かを指定する真偽値です。

ただ実際には、Iterableの実装クラスに、その性質にぴったりのStreamを作るメソッドを作ったほうが、使いやすいでしょう。例えば、本記事でも取り上げましたが、Stringのchars()メソッドやBufferedReaderのlines()メソッドのように。なので、StreamSupport.streamを使ったStream化は、未知のIterableあるいは実装をいじれないIterableなどに対する最終手段として考えたほうが良さそうです。

第1引数が、Spliteratorを生成するSupplierを受け取るオーバーライドもあります。正直、使いどころがまだよく分かりません。

List<String> source = Arrays.asList("a", "b", "c");
Supplier<Spliterator<String>> x = () -> source.spliterator();
int characteristics = Spliterator.SIZED | Spliterator.IMMUTABLE;
Stream<String> stream= StreamSupport.stream(x, characteristics, isParallel);
stream.forEach(System.out::println); // a, b, c

プリミティブ用にも、Spliteratorを受け取るStreamSupport.intStreamやStreamSupport.longStreamなどのメソッドが用意されています。そうしたプリミティブのSpliteratorを作成することはそうそうないと思いましたので、サンプルは省略いたします。

人気の投稿