map で変換する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • stream.map(x -> 変換結果) で各要素を変換、新しい Stream を返す
  • 戻り値の型が変わる ─ Stream<User>Stream<String> のように
  • 中間操作 (遅延評価)、終端操作 (toList collect) まで動かない
  • mapToInt mapToLong でプリミティブ 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 を仕事で書く現場では、こうした「一括変換 + 集計」のパターンは毎日のように登場します。

diagram (will load when visible)

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());

namesStream<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 -> Double

String::length のような メソッド参照 も、s -> s.length() と同じ意味で使えます。慣れてきたら短く書けて読みやすくなります。

map は中間操作なので、map 単体では何も実行されません。sum() count() collect() forEach() などの 終端操作 を呼んで初めて、ストリームが流れ始めて変換が行われます。これを 遅延評価 と呼びます。Stream を作って map を 5 回くらいつなげても、終端操作を呼ばなければ画面には何も出ません。

Stream.mapIntStream.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 への ボクシングアンボクシング が裏で行われていて、要素数が多くなるとオーバーヘッドが無視できません。一方、IntStreamintint のまま扱うので、ボクシングが発生せず、sum() average() max() などの集計メソッドもそのまま使えます。

数値の集計をしたいときは IntStream LongStream DoubleStream を使うのが基本、というのが現代の Java の作法です。Stream<Integer>.mapToInt(Integer::intValue).sum() のように落としてから集計するスタイルも頻出するので、ぜひ覚えてください。

mapToInt mapToDouble mapToObj の使い分け

Stream<T> から数値の集計をしたいとき、ふつうの mapInteger を返すと 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 = 17

mapToInt の引数は 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 は並列ストリームで予期しない順序で動いて、バグの温床になります
  • 型が合わずコンパイルエラーIntStreammapint -> int しか受け付けません。IntStream.of(1, 2, 3).map(x -> "num=" + x) のように String を返そうとすると怒られます。型を変えたいときは mapToObj を使うのが正解です
  • null を返す mapmap(x -> something(x)) の中で null を返すと、後続の処理で NullPointerException が出やすくなります。null を返す可能性があるなら、Optional でラップするか filter(Objects::nonNull) で前処理するのが安全です

もう一つ細かい話として、map のラムダは「純粋関数」であるべき、という考え方があります。同じ入力に対して常に同じ出力を返し、外部の状態を変えない、という関数です。これを守るとコードが読みやすくなり、並列ストリームへの切り替えも安全になります。

peekmap とよく似ていますが、要素を変換せずに「のぞき見る」だけのメソッドです。ログ出力やデバッグ専用と思って、本番処理では使わないのが無難です。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 を扱うときに flatMapmap を使うと Stream<Optional<T>> になってしまう

パフォーマンス考慮事項

  • 遅延評価toList まで実行されない、findFirst でショートカット
  • プリミティブ専用mapToIntIntStream、ボクシングなしで 3〜10 倍速い
  • map の連鎖は 1 パス ─ 内部的には全要素を 1 周しか回らない
  • 並列化parallelStream().map(...) で CPU バウンドな変換を並列実行
この章のポイント

ここまでの要点 stream.map(Function) で各要素を変換、新しい Stream を返す。プリミティブには mapToIntOptional には flatMap

やってみよう

今回の課題は、整数配列の各要素を 2 乗してから合計を返す Solution.squareSum(int[] arr) を書くことです。Stream API の map を使う典型例なので、次の手順で組み立ててみてください。

  1. 引数の arrArrays.stream(arr)IntStream に変える
  2. .map(x -> x * x) で各要素を 2 乗する
  3. .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 つができることを確認しましょう。

  1. map の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. map とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Arrays.stream(arr)int[]IntStream に変換すること
  2. map(x -> x * x) を使って各要素を 2 乗する Stream の中間操作を使うこと (生の for ループで合計するのは NG)
  3. 終端操作 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

ヒント

main.java
main.java
学習モード

メモ

map で変換する

⌘S で保存