【Java】Stream API チートシート
はじめに
新卒でエンジニア研修を受けている者です。
最近勉強の一環でAtCoderやpaizaを始めました。JavaのStream APIについては毎回調べてしまうため、自分用のメモとしてまとめています。
本記事はJavaの学習備忘録としてまとめたものです。よりスマートなアプローチがあれば、ぜひコメントでご教示ください!
1. 生成フェーズ
arrayやArrayList、連番リストなど、それぞれのデータ形式からStream APIが利用できる形に変換する方法をまとめました。
1-1. コレクション(ListやSet)から生成する
List<String> list = List.of("A", "B", "C");
list.stream().forEach(System.out::println);
/*
出力結果:
A
B
C
*/
1-2. 配列や個別の値から生成する
String[] array = {"A", "B", "C"};
Arrays.stream(array).forEach(System.out::println);
// 値を直接指定する場合(java.util.stream.Streamのimportが必要)
Stream.of("A", "B", "C").forEach(System.out::println);
1-3. 指定した回数のループ処理を行う(for文のような反復処理)
インデックスを使った従来の for (int i = 0; i < 5; i++) のような処理を記述します。
// 0から4までの数値ストリームを生成し、処理を行う
IntStream.range(0, 5).forEach(System.out::println);
/*
出力結果:
0
1
2
3
4
*/
補足:
System.out::println で使用されている :: は、「メソッド参照」と呼ばれる記法です。
メソッド参照は既存メソッド呼び出しをラムダ式より簡潔に表現するための記法です。
たとえば x -> System.out.println(x) は System.out::println と書けます。
2. 中間操作フェーズ
生成したStreamに対して、条件による絞り込みや型の変換などを行います。
2-1. 条件に一致するデータだけを残す(絞り込み)
条件式を与え、結果が true になる要素だけを後続に流します。
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers);
/*
出力結果:
[2, 4]
*/
2-2. リストからnullの要素を除外する
List<String> data = Arrays.asList("A", null, "B", null, "C");
List<String> nonNullData = data.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println(nonNullData);
/*
出力結果:
[A, B, C]
*/
2-3. 要素から特定のプロパティや値を抽出する
文字列のリストから各要素の文字数を数えるなど、要素を別の値に変換(マッピング)します。
List<String> list = List.of("A", "BB", "CCC");
List<Integer> lengths = list.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths);
/*
出力結果:
[1, 2, 3]
*/
2-4. リストの要素の型を変換する(StringからIntegerなど)
文字列の数値を、計算可能な型に変換するユースケースです。
List<String> numberStrings = List.of("10", "20", "30");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
System.out.println(numbers);
/*
出力結果:
[10, 20, 30]
*/
補足:
map(Integer::parseInt) は s -> Integer.parseInt(s) と同義です。map は単なる値の抽出だけでなく、オブジェクトの型を別の型へ変換(マッピング)する役割も持ちます。
3. 終端操作フェーズ
加工したデータをListやMapに集約したり、条件判定を行ったりします。
3-1. 処理結果を新しいListや配列として受け取る
List<String> list = List.of("A", "B", "C");
// 1. 新しいListとして受け取る (Collectors.toList)
List<String> result = list.stream()
.collect(Collectors.toList());
System.out.println(result);
// 2. Java 16以降ではtoList()も利用可能
List<String> resultJava16 = list.stream().toList();
System.out.println(resultJava16);
// 3. 配列(Array)として受け取る
String[] array = list.stream().toArray(String[]::new);
補足:
Collectors.toList() と stream.toList() では、後者のほうが簡潔に書けますが、(変更不可なリストが返されるなど)機能が制限されます。出典:JDK 16 : stream.toList() に見るAPI設計の難しさ(A Memorandum/Naotsugu)
3-2. オブジェクトのリストから、IDなどをキーにしたMapを作成する
リストのデータを検索しやすくするために、一意のIDなどをキーとする Map に変換します。
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user
));
3-3. 特定の条件でデータをグループ化する
部署IDなどをキーにして、ユーザー一覧を部署ごとのリストに分けたい場合に使用します。
Map<Integer, List<User>> usersByDept = users.stream()
.collect(Collectors.groupingBy(User::getDepartmentId));
補足:
groupingBy を使用すると、指定したキーで自動的にMapが生成され、該当する要素が List として格納されます。従来のfor文では、Mapにキーが存在するか確認して空リストを追加する処理が必要です。Stream APIを使えば、これらの定型処理をすべて1行で簡潔に記述できます。
3-4. 条件を満たすものと満たさないものの2つのグループに分割する
テストの点数から合格者と不合格者を同時に抽出する場面のように、条件式の真偽(true / false)でデータを二分する操作です。
// 点数が80点以上かどうかで分割する
Map<Boolean, List<Student>> partitioned = students.stream()
.collect(Collectors.partitioningBy(s -> s.getScore() >= 80));
List<Student> passed = partitioned.get(true); // 80点以上のリスト
List<Student> failed = partitioned.get(false); // 80点未満のリスト
Appendix: Java Stream APIとfor文の実行速度比較
ここまでチートシートを作成しましたが、AtCoderで本当に使用できるのか?TLEにはならないか?を確認するために、
ABC465のB問題「Parking 2」を例に、実行速度を比較してみました。
今回の簡易計測では、for文の方がStream APIより高速で、差は最大で約36マイクロ秒でした。
ただし、System.nanoTime() を使った簡易的な比較であり、入力規模やJVMの最適化状況によって結果は変わり得ます。
今回の入力例では、比較的小さい問題であれば Stream API も選択肢になりそうだと感じました。
検証に使用したコードと実行結果は以下の通りです。気になる方は展開してご覧ください。
検証コード
package draft.java.TestCode;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.IntStream;
public class Main {
/**
* 3つのテストケースに対して、100回実行した場合の平均時間を求めるメイン処理
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int testCaseCount = 1;
final int ITERATION = 100;
while(sc.hasNextInt()) {
int X = sc.nextInt(),
Y = sc.nextInt(),
L = sc.nextInt(),
R = sc.nextInt(),
A = sc.nextInt(),
B = sc.nextInt();
System.out.println("テストケース:" + testCaseCount++);
double forAvgTime = measureAverageTime(() -> solveByFor(X, Y, L, R, A, B), ITERATION);
double streamAvgTime = measureAverageTime(() -> solveByStream(X, Y, L, R, A, B), ITERATION);
System.out.println("for文での平均処理時間:" + forAvgTime + "ナノ秒");
System.out.println("Stream APIでの平均処理時間:" + streamAvgTime + "ナノ秒");
}
sc.close();
}
/**
* for文での解決用プログラム
*/
private static long solveByFor(int X, int Y, int L, int R, int A, int B) {
long total = 0;
for(int i = A; i < B; i++) {
total += (L <= i && i < R) ? X : Y;
}
return total;
}
/**
* stream APIでの解決用プログラム
*/
private static long solveByStream(int X, int Y, int L, int R, int A, int B) {
return IntStream.range(A, B)
.mapToLong(i -> (L <= i && i < R) ? X : Y)
.sum();
}
/**
* 平均処理速度計算用プログラム<br>
* supplierを利用して関数を渡しています。
*/
private static double measureAverageTime(Supplier<Long> task, int iterations) {
long startTime = System.nanoTime();
for(int i = 0; i < iterations; i++) {
task.get();
}
long endTime = System.nanoTime();
return (double) (endTime - startTime) / iterations;
}
}
インプット内容
700 300 9 17 7 21
600 500 9 17 17 20
900 200 12 14 11 13
出力結果
テストケース:1
for文での平均処理時間:442.0ナノ秒
Stream APIでの平均処理時間:36607.0ナノ秒
テストケース:2
for文での平均処理時間:187.0ナノ秒
Stream APIでの平均処理時間:2726.0ナノ秒
テストケース:3
for文での平均処理時間:633.0ナノ秒
Stream APIでの平均処理時間:3242.0ナノ秒