Java Stream APIの使い方

Stream APIとは?

Stream APIは、Java SE 8で追加された反復(iteration)の拡張APIです。これまでコレクションで行っていた複雑な処理を、わかりやすいコードで書けるようになります。
ここではラムダ式の説明はしないため、ラムダ式を知らない場合は、先にLambda関数を理解してから確認してください。

streamは、コレクションの要素へのアクセスを可能にするもので、コレクションの別の形と考えることができます。

基本的な流れ

ストリームの構造は大きく3つに分かれます。

  1. コレクションからStreamを生成します。
  2. streamに対して必要なだけ「中間操作」を実行し、コレクションの内容をまとめて変換します。
  3. 「終端操作」で、変換したコレクションの内容に対して処理を適用します。

実際の使い方を表すと、Stream生成().中間操作().終端操作()のように書きます。

それでは、実際の使用例を見てみましょう。次の例では「1から5の中で偶数だけを表示」します。

Stream APIを使用しない場合

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
    if (i % 2 == 0) {
        System.out.println(i);
    }
}

Stream APIを使用した場合

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream() // Stream生成
        .filter(i -> i % 2 == 0) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

処理の違いを見てみましょう。Stream APIを使用しない場合は、「2で割った余りが0の場合だけ表示する」構造になっています。Stream APIを使用した場合は、「2で割り切れる要素を集め、その要素をすべて表示する」構造になっています。

2つの処理を分けられるようになりました。これにより可読性と部品性が高まり、コードの抽象化も可能になります。

今回の例ではstreamを取得し、中間操作としてfilterメソッドを使用しています。名前のとおり、コレクションの要素をフィルタリングします。filterメソッドにはT -> booleanのようなラムダ式を渡します。(※ Tの意味がわからない場合は、ジェネリクスを確認してください。)ラムダ式がtrueになる要素だけを残したstreamを返します。

終端操作としてforEachメソッドを使用しています。これも名前のとおり、「要素を1つずつ取り出す処理を実行する」メソッドです。他の言語でもなじみのある構文でしょう。Javaでは拡張for文に相当します。

この例では、「2で割った余りが0になるものだけ」を取得する中間操作と、「1つずつ画面に出力する」終端操作に分かれています。

中間操作の結果は、処理を適用した後にstreamとして返されるため、例のように連続して参照する形で書くこともできます。

もちろん、次のように書くこともできます。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream(); // Stream生成
Stream<Integer> stream2 = stream.filter(i -> i % 2 == 0); // 中間操作
stream2.forEach(i -> System.out.println(i)); // 終端操作

連続して書くほうが可読性がよいため、特別な理由がなければ連続して書くようにしましょう。

中間操作

よく使用される代表的な中間操作について説明します。

filter

前の例で触れたように、フィルタリングするための中間操作です。引数にはT -> booleanのようなラムダ式を渡します。条件式がtrueの要素だけを収集します。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() //  Stream生成
        .filter(i -> i % 2 == 0) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

map

mapの引数にはT -> Uのようなラムダ式を渡します。要素を変換する中間操作です。

ストリーム内の各要素に1つずつアクセスし、パラメータとして渡した関数を実行した後、終端操作で指定した形式で返します。

次の例のように、値を2倍にすることもできます。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream生成
        .map(i -> i * 2) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

次の例のように、IntegerからStringへ別の形式を返すこともできます。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream生成
        .map(i -> "요소는" + i + "입니다.") // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

また、次のようなAnimalインターフェースがあるとします。

interface Animal {
    /**
     * 鳴き声を返します。
     */
    String getCry();
}

動物リストにmapを使用して、鳴き声を出力することもできます。

List<Animal> animalList = Arrays.asList(dog, cat, elephant);
animalList.stream() // Stream生成
        .map(animal -> animal.getCry()) // 中間操作
        .forEach(cry -> System.out.println(cry)); // 終端操作

flatMap

flatMapはストリームの平坦化とも呼ばれます。flatMapは、データが2次元配列またはリストとして存在する場合に、それを1次元に変換します。

次のコードで確認してみましょう。

List<List<String>> alphabets = Arrays.asList(Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f"));

List<String> result = alphabets.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());

System.out.println(result);

Output:

[a, b, c, d, e, f]

sorted

sortedの引数には(T, T) -> intのようなラムダ式を渡します。要素を並べ替える中間操作です。要素を2つずつ取り、ラムダ式に渡します。戻り値が正数なら降順、負数なら昇順で並べ替えられます。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream生成
        .sorted((a, b) -> Integer.compare(a, b)) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

より簡単な並べ替え(昇順、降順)の方法としては、次の例のようにComparatorインターフェースのnaturalOrder()reverseOrder()を渡すのが最もわかりやすいでしょう。

List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
integerList.stream() // Stream生成
        .sorted(Comparator.reverseOrder()) // 中間操作
        .forEach(i -> System.out.println(i)); // 終端操作

以上、よく使われる代表的な3つの中間操作について見てきました。このほかにもさまざまなものがあるため、興味があれば調べてみてください。

終端操作

forEach

forEachの引数には(T) -> voidのようなラムダ式を渡します。すべての要素を1つずつ取り出し、何らかの処理を行う終端操作です。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

numbers.stream()
        .map(x -> x + 3) // 4を加算します。
        .filter(x -> x % 2 == 0) // 偶数だけをフィルタリングします。
        .limit(3) // 3個まで取得します。
        .forEach(System.out::println);

Output:

4
6
8

forEachを呼び出すと、その呼び出しによってストリーム全体が消費されます。

peek

呼び出されるとストリーム全体を消費するforEachと比べ、peekは実際にはストリームの要素を消費しません。peekは確認した要素をそのままパイプラインの次の操作へ渡し、各操作の前後の中間値を出力します。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

List<Integer> result = numbers.stream()
        .peek(v -> System.out.println("From stream: " + v)) // 最初に消費した要素を出力

        .map(v -> v + 3)
        .peek(v -> System.out.println("Mapped value: " + v)) // map実行後の結果を出力

        .filter(v -> v % 2 == 0)
        .peek(v -> System.out.println("Filtered value: " + v)) // filter実行後の結果を出力

        .limit(3)
        .peek(v -> System.out.println("limited value: " + v)) // limit実行後の結果を出力

        .collect(Collectors.toList());

System.out.println(result);

Output:

From stream: 1
Mapped value: 4
Filtered value: 4
limited value: 4
From stream: 2
Mapped value: 5
From stream: 3
Mapped value: 6
Filtered value: 6
limited value: 6
From stream: 4
Mapped value: 7
[4, 6]

さまざまなStreamの例

文字列(String)から数字ではない値だけを抽出し、再び文字列(String)へ変換する

String input = "1Ca2Adf34eB342";

String output = input.chars()
        .mapToObj(ch -> (char) ch)// Stream<Character>
        .sorted()
        .filter(ch -> !Character.isDigit(ch))
        .map(Object::toString)
        .collect(Collectors.joining());

System.out.println(output);

Output:

ABCadef

2つのリストを組み合わせてペアにする

List<Integer> numberArray1 = Arrays.asList(1, 2);
List<Integer> numberArray2 = Arrays.asList(3, 4, 5);

List<int[]> pairs = numberArray1.stream()
        .flatMap(i -> numberArray2.stream()
                .map(j -> new int[]{i, j}))
        .collect(Collectors.toList());

for (int[] a : pairs) {
        System.out.println(Arrays.toString(a));
}

Output:

[1, 3]
[1, 4]
[1, 5]
[2, 3]
[2, 4]
[2, 5]

Stringから数字を抽出して合計(sum)を求める

String input = "1Ca2Adf9";

int sum = input.chars()
        .filter(Character::isDigit)
        .map(a -> Character.digit(a, 10))
        .sum();

System.out.println(sum); // 1 + 2 + 9

Output:

12

文字列(String)がnullではない場合だけjoiningを実行する

String front = "abc";
String middle = null;
String end = "ghi";

String output = Stream.of(front, middle, end)
        .filter(Objects::nonNull)
        .collect(Collectors.joining());

System.out.println(output);

Output:

abcghi