map で変換する
このレッスンで分かること
stream.map(x -> 変換結果)で各要素を変換、新しい Stream を返す- 戻り値の型が変わる ─
Stream<User>→Stream<String>のように- 中間操作 (遅延評価)、終端操作 (
toListcollect) まで動かないmapToIntmapToLongでプリミティブ Stream への変換 (ボクシング回避)
map で変換する とは
Stream API の map を使って要素を別の値に変換する書き方を学び、IntStream.map との違いや mapToInt の使い分けまで踏み込んで身につけよう。
Stream の map ─ 「全要素を一括変換」する道具
前のレッスンでは Stream の生成と forEach の使い方を見ました。今度は Stream API の中でも特に出番の多い中間操作、map を学んでいきます。map は「ストリームを流れてくる各要素を、別の値に変換する」操作です。for ループを書かずに、配列 や List の中身を一気に変換できるのが魅力です。
たとえば「整数の配列を全部 2 乗してから合計したい」というニーズを考えてみましょう。古い書き方なら for で 1 要素ずつ取り出して sum += arr[i] * arr[i]; のように書きますが、Stream API なら次のように 1 行で表現できます。
Java
int total = Arrays.stream(arr).map(x -> x * x).sum();ここで使われているのが map です。x -> x * x というラムダ式を渡すと、ストリームを流れる 1 2 3 がそれぞれ 1 4 9 に化けて、続く sum() でまとめて足し合わされる、というイメージです。Java を仕事で書く現場では、こうした「一括変換 + 集計」のパターンは毎日のように登場します。
mapは「写像」と訳されます。数学の関数f(x) = x * xのように、入力 1 個に対して出力 1 個を返す、という性質をストリーム全体に適用するイメージで覚えると腑に落ちます。要素の 個数は変わらない のがポイントです。要素を絞り込みたいときは別のfilterを使います。
map(Function<T, R>) の基本
Stream<T> の map のシグネチャは <R> Stream<R> map(Function<? super T, ? extends R> mapper) です。型がややこしく見えますが、実は言っていることは単純です。「T を受け取って R を返す関数を渡してね、そしたら Stream<R> を返してあげるよ」というだけのことです。
Java
Stream<String> names = Stream.of("alice", "bob", "carol");
Stream<Integer> lengths = names.map(s -> s.length());names は Stream<String>、map に渡しているラムダ s -> s.length() は String から int (オートボクシングで Integer) を返す関数なので、結果は Stream<Integer> になります。元の型と変換後の型が違ってもよい、これが map の便利なところです。
もう少し色々な型を試してみます。
Java
Stream<String> names = Stream.of("alice", "bob");
Stream<String> upper = names.map(s -> s.toUpperCase()); // String -> String
Stream<Integer> sizes = Stream.of("a", "bb").map(String::length); // String -> Integer
Stream<Double> prices = Stream.of(100, 200).map(n -> n * 1.1); // Integer -> DoubleString::length のような メソッド参照 も、s -> s.length() と同じ意味で使えます。慣れてきたら短く書けて読みやすくなります。
mapは中間操作なので、map単体では何も実行されません。sum()count()collect()forEach()などの 終端操作 を呼んで初めて、ストリームが流れ始めて変換が行われます。これを遅延評価と呼びます。Streamを作ってmapを 5 回くらいつなげても、終端操作を呼ばなければ画面には何も出ません。
Stream.map と IntStream.map の違い
ここで地味だけど大事な話があります。Stream API には、オブジェクト用の Stream<T> と、プリミティブ用の IntStream LongStream DoubleStream という別ファミリーがあって、map の振る舞いがちょっとずつ違います。
Java
// オブジェクト版
Stream<Integer> a = Stream.of(1, 2, 3);
Stream<Integer> b = a.map(x -> x * x); // Stream<Integer> のまま
// プリミティブ版
IntStream c = IntStream.of(1, 2, 3);
IntStream d = c.map(x -> x * x); // IntStream のまま、int のまま見た目はそっくりですが、内部での扱いが違います。Stream<Integer> の方は要素を int で計算するたびに Integer への ボクシング と アンボクシング が裏で行われていて、要素数が多くなるとオーバーヘッドが無視できません。一方、IntStream は int を int のまま扱うので、ボクシングが発生せず、sum() average() max() などの集計メソッドもそのまま使えます。
数値の集計をしたいときは
IntStreamLongStreamDoubleStreamを使うのが基本、というのが現代の Java の作法です。Stream<Integer>を.mapToInt(Integer::intValue).sum()のように落としてから集計するスタイルも頻出するので、ぜひ覚えてください。
mapToInt mapToDouble mapToObj の使い分け
Stream<T> から数値の集計をしたいとき、ふつうの map で Integer を返すと Stream<Integer> のままで、sum() が直接呼べません。そんなときに使うのが mapToInt mapToLong mapToDouble という「型を変えて落とす」系の map です。
Java
List<String> words = List.of("apple", "banana", "cherry");
int totalLen = words.stream()
.mapToInt(String::length) // Stream<String> -> IntStream
.sum(); // 5 + 6 + 6 = 17mapToInt の引数は ToIntFunction<T> という型で、「T を受け取って int を返す関数」を期待します。返り値は IntStream になるので、続けて sum() average() max() などのプリミティブ集計メソッドがそのまま呼べる、という流れです。
逆の方向に「IntStream から Stream<T> に戻したい」というときは mapToObj を使います。
Java
List<String> labels = IntStream.rangeClosed(1, 3)
.mapToObj(i -> "No." + i)
.toList();
// ["No.1", "No.2", "No.3"]今回の課題の本筋は IntStream.map + sum() で十分に解けるので、まずはここで紹介した 3 種類 (map / mapToInt / mapToObj) の存在を頭に入れておきましょう。プロの現場では使い分けが普通にできて当然、というレベルで聞かれます。
よくある間違い
map は強力な反面、初学者がやりがちな落とし穴がいくつかあります。よくあるパターンを 3 つ紹介します。
mapの中で副作用を書く —map(x -> { System.out.println(x); return x; })のように、変換のついでにprintlnやフィールド書き換えをするのは Stream の作法に反します。表示やログ出力はpeekか終端操作のforEachで行うのがルールです。副作用入りのmapは並列ストリームで予期しない順序で動いて、バグの温床になります- 型が合わずコンパイルエラー —
IntStreamのmapはint -> intしか受け付けません。IntStream.of(1, 2, 3).map(x -> "num=" + x)のようにStringを返そうとすると怒られます。型を変えたいときはmapToObjを使うのが正解です - null を返す
map—map(x -> something(x))の中でnullを返すと、後続の処理でNullPointerExceptionが出やすくなります。null を返す可能性があるなら、Optionalでラップするかfilter(Objects::nonNull)で前処理するのが安全です
もう一つ細かい話として、map のラムダは「純粋関数」であるべき、という考え方があります。同じ入力に対して常に同じ出力を返し、外部の状態を変えない、という関数です。これを守るとコードが読みやすくなり、並列ストリームへの切り替えも安全になります。
peekはmapとよく似ていますが、要素を変換せずに「のぞき見る」だけのメソッドです。ログ出力やデバッグ専用と思って、本番処理では使わないのが無難です。peek内で副作用を書くのも、本来はテスト用途以外では推奨されません。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- Entity → DTO 変換 ─
users.stream().map(this::toDto).toList()の定番パターン - フォーマット変換 ─
lines.map(String::trim).map(String::toLowerCase) - 統計の前処理 ─
orders.stream().mapToInt(Order::getAmount).sum() - フィールド抽出 ─
users.stream().map(User::getEmail).distinct().toList()
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
map内で副作用 ─mapは純粋な変換であるべき、副作用はforEachか別の場所へmap連結が長すぎる ─ 3 段以上の map が並ぶと読みづらい、privateメソッドに切り出す- プリミティブで
mapToIntを使わない ─Stream<Integer>のままだとボクシングのオーバーヘッド mapの戻り値でOptionalを扱うときにflatMap─mapを使うとStream<Optional<T>>になってしまう
パフォーマンス考慮事項
- 遅延評価 ─
toListまで実行されない、findFirstでショートカット - プリミティブ専用 ─
mapToIntでIntStream、ボクシングなしで 3〜10 倍速い mapの連鎖は 1 パス ─ 内部的には全要素を 1 周しか回らない- 並列化 ─
parallelStream().map(...)で CPU バウンドな変換を並列実行
ここまでの要点
stream.map(Function) で各要素を変換、新しい Stream を返す。プリミティブには mapToInt、Optional には flatMap。
やってみよう
今回の課題は、整数配列の各要素を 2 乗してから合計を返す Solution.squareSum(int[] arr) を書くことです。Stream API の map を使う典型例なので、次の手順で組み立ててみてください。
- 引数の
arrをArrays.stream(arr)でIntStreamに変える .map(x -> x * x)で各要素を 2 乗する.sum()で合計を取り、returnする
空の配列 [] を渡されたときは、IntStream が空のストリームになり、sum() の結果は 0 になります。特別な分岐を書かなくても自然に動くのが Stream の気持ちいいところです。要素 1 個 [5] のときも、5 * 5 = 25 がそのまま返ります。
余裕があれば、同じ問題を Stream<Integer> で書くとどうなるか、for ループで書くとどうなるかを並べて比較してみるのもおすすめです。Stream API は「書ける」だけでなく、「同じ処理を何通りもの書き方で表現できる」状態を目指すと、現場で読みづらいコードに出会ったときも落ち着いて読み解けるようになります。
次のレッスンでは、map と並んで頻出する filter を扱います。map が「変換」なら filter は「絞り込み」、組み合わせると for + if のような処理を Stream で表現できるようになります。
よくある質問
Q. map は for ループより速いですか?
A. CPython では大差ない場合が多いですが、map(int, strs) のように組み込み関数を渡すと内包表記より僅かに速いことがあります。JavaScript でも明確な差は出にくいため、可読性で選ぶのが基本で、配列が巨大な場合のみベンチマークしましょう。
Q. map と forEach の違いは?
A. map は新しい配列を返し、forEach は副作用を起こすだけで戻り値が undefined です。「変換後の配列を作る」なら map、「ログ出力や DOM 更新だけ」なら forEach を使うと意図が伝わります。途中で抜けたい場合は for...of + break を選びましょう。
Q. map の戻り値が思った形になりません
A. map は元と同じ長さの配列を返します。条件で要素数を変えたいなら filter を先にかけるか、flatMap で空配列 [] を返した分を平らにできます。インデックスが必要なら map((v, i) => ...) のように第 2 引数で受け取ってください。
次のレッスン
次は reduce で集約する で、reduce で集約する を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- map の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. map とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Arrays.stream(arr)でint[]をIntStreamに変換することmap(x -> x * x)を使って各要素を 2 乗する Stream の中間操作を使うこと (生の for ループで合計するのは NG)- 終端操作
sum()を呼んでintの合計をreturnすること
入出力例
test-cases.txt
squareSum([1,2,3]) → 14
squareSum([]) → 0
squareSum([5]) → 25
squareSum([2,4]) → 20
squareSum([0,0,0]) → 0