JavaのコードやAIが生成したコードを見ていると、こんな処理が出てくる。
List<ProjectResponse> responses = projects.stream()
.map(project -> new ProjectResponse(
project.getId(),
project.getName(),
project.getStatus()
))
.toList();
最初に見たとき、
stream()は何をしている?
map()は何を変換している?
toList()すると何が入る?
という部分が曖昧だった。
ただ、自分が実務で使っていたfor文と対応させるとかなり理解しやすかった。
現場でよく見ていたfor文
実務では、DBから取得したデータを別のオブジェクトへ詰める処理をよく見ていた。
List<TargetEntity> targetList = new ArrayList<>();
for (SourceEntity source : sourceList) {
TargetEntity target = new TargetEntity(
source.getId(),
source.getName(),
source.getStatus()
);
targetList.add(target);
}
処理としては、
sourceListから1件取り出す
↓
必要な値をgetterで取得する
↓
TargetEntityを作る
↓
targetListへ追加する
という流れ。
同じ処理をStreamで書く
Streamを使うと、こう書ける。
List<TargetEntity> targetList = sourceList.stream()
.map(source -> new TargetEntity(
source.getId(),
source.getName(),
source.getStatus()
))
.toList();
最初は別物に見えたが、for文と対応させると分かりやすい。
stream() は処理対象にする
sourceList.stream()
は、
sourceListの要素を
Streamとして処理できる状態にする
という意味。
この時点ではまだ List<TargetEntity> ができたわけではない。
この後に map() や filter() などの処理をつなげられる。
map() は1件を別のデータへ変換する
.map(source -> new TargetEntity(
source.getId(),
source.getName(),
source.getStatus()
))
これは、
SourceEntityを1件受け取る
↓
getterで必要な値を取る
↓
TargetEntityを作る
という処理。
つまり、
SourceEntity
↓
TargetEntity
への変換になる。
map() は、
1件のデータを
別のデータへ変換する
と考えると分かりやすかった。
for文の変換部分と対応している
for文では、
TargetEntity target = new TargetEntity(
source.getId(),
source.getName(),
source.getStatus()
);
としていた。
Streamでは、
.map(source -> new TargetEntity(
source.getId(),
source.getName(),
source.getStatus()
))
になる。
自分がfor文の中でやっていた、
1件取り出す
↓
別オブジェクトを作る
という部分が map() に対応している。
toList() するとListになる
次に分かりにくかったのが、
.toList();
だった。
map() の時点では、結果はまだListではない。
たとえば、
sourceList.stream()
.map(source -> new TargetEntity(...))
の結果はイメージとして、
Stream<TargetEntity>
になっている。
ここで、
.toList()
とすると、
Stream<TargetEntity>
↓
List<TargetEntity>
になる。
そのため、
List<TargetEntity> targetList = sourceList.stream()
.map(source -> new TargetEntity(...))
.toList();
と受け取ることができる。
for文の add() と対応させる
for文では、
targetList.add(target);
として、1件ずつListへ追加していた。
Streamでは、
.toList();
によって、map() で変換されたデータをまとめてListにする。
対応させると、
for文
1件取り出す
↓
TargetEntityに変換
↓
targetList.add(target)
に対して、
Stream
stream()
↓
map(...)
↓
toList()
となる。
この対応でかなり理解しやすくなった。
なぜProjectResponseへ変換するのか
Web APIでは、DBから取得したデータをResponse用のオブジェクトへ変換することがある。
たとえば、DB側で使う Project が、
class Project {
Long id;
String name;
String status;
String internalMemo;
LocalDateTime createdAt;
}
だったとする。
しかしAPIで返したいのは、
class ProjectResponse {
Long id;
String name;
String status;
}
だけかもしれない。
そこで、
.map(project -> new ProjectResponse(
project.getId(),
project.getName(),
project.getStatus()
))
と変換する。
理由は、
DBから取ってきたProjectを
そのまま外に返さず、
APIで返したい形に整えるため
だった。
単に型を変えているだけではなく、
DB内部で扱うデータ
↓
APIとして外に返すデータ
へ役割を変えている。
これによって、
内部用の項目を返さない
必要な項目だけ返す
DBの構造とAPIの構造を分ける
ことができる。
全体をもう一度見る
List<ProjectResponse> responses = projects.stream()
.map(project -> new ProjectResponse(
project.getId(),
project.getName(),
project.getStatus()
))
.toList();
この処理は、
projects
DBなどから取得したProject一覧
↓
stream()
Streamとして処理する
↓
map()
ProjectをProjectResponseへ変換する
↓
toList()
変換後のProjectResponseをListにまとめる
↓
List<ProjectResponse> responses
レスポンス用の一覧として受け取る
という流れ。
まとめ
自分が現場で見慣れていたfor文は、
List<TargetEntity> targetList = new ArrayList<>();
for (SourceEntity source : sourceList) {
TargetEntity target = new TargetEntity(...);
targetList.add(target);
}
という形だった。
Streamでは、
List<TargetEntity> targetList = sourceList.stream()
.map(source -> new TargetEntity(...))
.toList();
と書ける。
対応させると、
for文で1件ずつ処理する
↓
stream()
別オブジェクトへ変換する
↓
map()
Listへ追加していく
↓
toList()
となる。
特に、
map()は「変換」
toList()は「変換結果をListにまとめる」
と理解すると読みやすかった。
そしてResponseへ変換する理由も、
DBから取ったデータを
APIで返したい形に整えるため
と考えると、単なる書き方ではなく実務での意味まで理解しやすかった。