Streamの map() や toList() を理解してくると、
「では、for文よりStreamを使った方がいいのか?」
という疑問が出てきた。
実際にコードを見比べてみると、Streamだから保守性が高い、for文だから古い、という単純な話ではなかった。
今回は、どんな処理ならStreamが読みやすく、どんな処理ならfor文の方が読みやすいのかを整理する。
Streamはデータ加工と相性がいい
たとえば、
List<ProjectResponse> responses = projects.stream()
.filter(project -> project.isActive())
.map(project -> new ProjectResponse(
project.getId(),
project.getName()
))
.toList();
この処理は、
Project一覧
↓
activeなものだけ残す
↓
ProjectResponseへ変換
↓
Listにする
という流れ。
上から順番に読むだけで、何をしているか分かりやすい。
filter() は条件に合うものだけ残す
.filter(project -> project.isActive())
は、
projectがactiveなら残す
という意味。
つまり、一覧の絞り込み。
for文なら、
for (Project project : projects) {
if (!project.isActive()) {
continue;
}
// 処理
}
のような部分に近い。
Streamが向いている処理
Streamは特に、
一覧を条件で絞る
別の型へ変換する
並べ替える
集計する
新しいListを作る
といった処理と相性がいい。
たとえば、
List<ProjectResponse> responses = projects.stream()
.filter(Project::isActive)
.map(ProjectResponse::from)
.toList();
なら、
有効なProjectを残す
↓
ProjectResponseへ変換
↓
Listにする
という目的がコードにそのまま出ている。
for文の方が読みやすい処理もある
一方で、何でもStreamにすればいいわけではない。
たとえば、
projects.stream()
.map(project -> {
updateDatabase(project);
sendRequest(project);
project.setStatus("DONE");
return project;
})
.toList();
このコードでは、
DB更新
API呼び出し
状態変更
まで map() の中に入っている。
本来 map() はデータを別の形へ変換する処理として読むことが多い。
そこにDB更新やAPI呼び出しまで入ると、処理の目的が分かりにくくなる。
for文なら処理順をそのまま読める
同じような処理をfor文で書く。
for (Project project : projects) {
updateDatabase(project);
sendRequest(project);
project.setStatus("DONE");
}
これは、
DB更新
↓
API呼び出し
↓
status変更
とそのまま読める。
特に、
DB更新
外部API呼び出し
状態変更
例外処理
continue
break
などが多い処理では、for文の方が処理順を追いやすいことがある。
Streamだから保守性が高いわけではない
たとえば、
List<String> names = projects.stream()
.filter(Project::isActive)
.map(Project::getName)
.toList();
なら、
activeなProjectだけ残す
↓
nameへ変換
↓
Listにする
と非常に分かりやすい。
こうした処理ではStreamの方が意図を読み取りやすい。
一方、Streamの中に長い処理を書き始めると、
projects.stream()
.filter(project -> {
// 長い処理
return true;
})
.map(project -> {
// さらに長い処理
return project;
});
かえって読みづらくなる。
保守性を考えるなら、
Streamを使っているか
for文を使っているか
ではなく、
その処理の意図がコードから読み取れるか
を見る方が大事だと思った。
toList() の注意点
Java 16以降の Stream.toList() が返すListは変更不可。
たとえば、
List<ProjectResponse> responses = projects.stream()
.map(ProjectResponse::from)
.toList();
とした後に、
responses.add(newResponse);
と追加しようとすると、UnsupportedOperationException になる。
つまり、
変換した結果をそのまま読む
用途なら toList() は使いやすい。
一方、
後から要素を追加する
削除する
必要がある場合は注意が必要。
AIがStreamを書きやすい理由
AIが生成するJavaコードでは、Streamを見る機会が多いように感じる。
たとえば、
DBから取得したEntityを
Response DTOへ変換する
という依頼なら、
projects.stream()
.map(ProjectResponse::from)
.toList();
は、
変換する
↓
Listにする
という処理を短く表現できる。
そのため、こうしたデータ加工ではAIもStreamを選びやすいのだと思う。
ただし、
AIがStreamを書いた
=
Streamを採用するべき
ではない。
既存コードがfor文中心なら、その書き方に合わせた方が読みやすい場合もある。
チームのメンバーがStreamに慣れているかどうかも保守性に影響する。
自分の中での使い分け
Streamが向いていそうなのは、
絞り込み
変換
並べ替え
集計
新しいListの生成
といったデータ加工。
for文が向いていそうなのは、
DB更新
外部API呼び出し
状態変更
処理順が重要
continue / breakを使う
例外処理が複雑
といった処理。
まとめ
Streamを知ったとき、
for文より新しい書き方
のように感じた。
ただ、実際には役割が少し違う。
for文
→ 処理を繰り返す
Stream
→ データに対する加工を流れとして表現する
という違いがある。
特に、
絞る
↓
変換する
↓
Listにする
という処理ならStreamはかなり読みやすい。
一方、
DBを更新する
↓
APIを呼ぶ
↓
状態を変更する
のように処理手順そのものが重要なら、for文の方が読みやすい場合もある。
「Streamの方がモダンだから使う」ではなく、
この処理なら
どちらの方が意図を読み取りやすいか
で選ぶのがよさそうだと整理できた。