新コレクションクラスStream

新たなコレクションとしてjava.util.stream.Streamが追加された。これは予め格納されている要素がわかっている従来のコレクションと違い、先頭の要素から順に遅延評価されるリストだ。そのため、要素にはインデックスでアクセスするのではなく、ラムダ式を使用して先頭の要素から順次処理していくようなスタイルが基本となる。

以下のサンプルはListからStreamを生成し、要素をすべて大文字に変換したあと文字列長の大きい順にソートしたうえでコンソールに出力している。

List<String> list = Arrays.asList("Java", "Scala", "Groovy");

list.stream()
  .map(s -> s.toUpperCase())
  .sorted((a, b) -> b.length() - a.length()) 
  .forEach(System.out::println);

Streamに対してはこの他にもフィルタリング、グルーピングなど様々な処理が可能だ。

List<String> list = Arrays.asList("Java", "Scala", "JavaScript", "Groovy");

// "J"で始まる要素のみにフィルタリング
list.stream()
  .filter(s -> s.startsWith("J"))
  .forEach(System.out::println);  // => "Java"、"JavaScript"の順に表示

// 先頭の一文字が同一の要素をグルーピング
Map<Character, List<String>> map = list.stream()
  .collect(Collectors.groupingBy(s -> s.charAt(0)));
System.out.println(map.get('J')); // => [Java, JavaScript]
System.out.println(map.get('G')); // => [Groovy]
System.out.println(map.get('S')); // => [Scala]

// すべての要素が"J"で始まるかどうか
boolean result1 = list.stream().allMatch(s -> s.startsWith("J")); // => false
// "J"で始まるか要素があるかどうか
boolean result2 = list.stream().anyMatch(s -> s.startsWith("J")); // => true
// "J"で始まる要素がないかどうか
boolean result3 = list.stream().noneMatch(s -> s.startsWith("J")); // => false

reduce()メソッドを使用して集計処理を行うこともできる。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

// 初期値を指定せずに集計(戻り値はOptional<Integer>)
// ((((1 * 2) * 3) * 4) * 5)という計算が行われる
Optional<Integer> result = stream.reduce((a, b) -> a * b);

// 初期値を指定して集計(戻り値はInteger)
// (((((1 * 1) * 2) * 3) * 4) * 5)という計算が行われる
Integer result = stream.reduce(1, (a, b) -> a * b);

また、後述するIntStreamなどの場合はsum()メソッドを使用して合計値を求めることも可能だ。

IntStream stream = IntStream.of(1, 2, 3, 4 ,5);
int total = stream.sum(); // => 15

ここまでのサンプルコードで見てきたようにStreamに対する処理はメソッドチェーンで呼び出すことができるが、forEach()やreduce()などの終端メソッドを呼び出した後はそのStreamのインスタンスを再利用することはできないという点には注意が必要だ。たとえば以下のコードを実行するとIllegalStateExceptionが発生する。

Stream<String> stream = Arrays.asList("A", "B", "C").stream();
// 終端メソッドの呼び出し
stream.forEach(System.out::println);
// IllegalStateExceptionが発生する
stream.forEach(System.out::println);

なお、int、long、doubleといったプリミティブ型に対してはラッパー型への変換のオーバーヘッドを避けるためIntStream、LongStream、DoubleStreamといった専用のクラスが用意されている。

// 固定の要素からIntStreamを生成する
IntStream intStream1 = IntStream.of(1, 2, 3);

// 配列からIntStreamを生成する
int[] array = {1, 2, 3};
IntStream intStream2 = IntStream.of(array);

// 1から10までの値を返すIntStreamを生成する
IntStream intStream3 = IntStream.range(1, 10);

// Stream#mapToInt()メソッドでIntStreamに変換
Stream<String> stream = Stream.of("Java", "Scala", "JavaScript", "Groovy");
IntStream intStream4 = stream.mapToInt(s -> s.length());

Streamは以下のようにcollect()メソッドを使用して従来のコレクションに変換することができる。従来のコレクションとの変換に一手間かかるのは面倒だが、今後はネイティブにStreamを扱うことのできるライブラリやフレームワークも増えていくだろう。

List<String> list = stream.collect(Collectors.toList());