Java Stream APIの使い方
Stream APIとは?
Stream APIは、Java SE 8で追加された反復(iteration)の拡張APIです。これまでコレクションで行っていた複雑な処理を、わかりやすいコードで書けるようになります。
ここではラムダ式の説明はしないため、ラムダ式を知らない場合は、先にLambda関数を理解してから確認してください。
streamは、コレクションの要素へのアクセスを可能にするもので、コレクションの別の形と考えることができます。
基本的な流れ
ストリームの構造は大きく3つに分かれます。
- コレクションからStreamを生成します。
- streamに対して必要なだけ「中間操作」を実行し、コレクションの内容をまとめて変換します。
- 「終端操作」で、変換したコレクションの内容に対して処理を適用します。
実際の使い方を表すと、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