Java | Java 詳細・モダン文法:Stream API 深掘り – map の設計

Java Java
スポンサーリンク

map を一言でいうと

Stream#map は、
「ストリームの各要素に“変換処理”を 1 回ずつ適用し、その結果から“新しいストリーム”を作る中間操作」 です。

元の要素を別の形にしたいとき――
文字列を大文字にする、数値を 2 倍にする、オブジェクトから特定フィールドだけ取り出す――
そういう「一対一の変換」を担当するのが map です。

filter が「通すか落とすか」を決める門番なら、
map は「形を変える職人」です。


map のシグネチャと役割を正確に押さえる

Function と map の関係

map のシグネチャはこうです。

<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Java

ここで重要なのは、Function<? super T, ? extends R> という引数です。

Function<T, R>

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Java

という関数型インターフェースで、
T を 1 つ受け取って R を返す関数」= T -> R を表します。

つまり map は、

T -> R な関数(変換処理)を 1 つ受け取り、
それをストリームの全要素に適用して、R 型のストリームを返す」

というメソッドです。

ここでの設計のポイントは、
map 自体は「変換の枠組み」だけを持ち、
「具体的にどう変換するか」は Function(ラムダやメソッド参照)として外から渡す、という分離になっていることです。


一番シンプルな map の例から設計感覚をつかむ

例1:文字列を大文字に変換する

import java.util.List;

public class MapBasic {
    public static void main(String[] args) {
        List<String> names = List.of("alice", "bob", "charlie");

        List<String> upper =
                names.stream()
                     .map(s -> s.toUpperCase())
                     .toList();

        System.out.println(upper); // [ALICE, BOB, CHARLIE]
    }
}
Java

ここでの map の役割は、

String を受け取って String を返す変換(小文字 → 大文字)」

を、全要素に適用することです。

ラムダ s -> s.toUpperCase()Function<String, String> です。
map はこの関数を、ストリームの各要素に 1 回ずつ適用し、
結果から新しい Stream<String> を作っています。

例2:数値を 3 倍にする

import java.util.List;

public class MapNumberExample {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4);

        List<Integer> triple =
                nums.stream()
                    .map(n -> n * 3)
                    .toList();

        System.out.println(triple); // [3, 6, 9, 12]
    }
}
Java

ここでは、

Integer を受け取って Integer を返す変換(n → n * 3)」

map に渡しています。

どちらの例も、
「元のストリームは変更されず、新しいストリームが作られる」
という点が重要です。


map の「設計」を考える視点

変換処理を“その場で書く”か、“メソッドに切り出す”か

最初はラムダをその場で書くので十分です。

.map(user -> user.getName())
Java

ただ、変換が複雑になってきたら、
「名前を付けて外に出す」ことを考えます。

private static String toDisplayName(User u) {
    return u.getLastName() + " " + u.getFirstName();
}

List<String> displayNames =
        users.stream()
             .map(MapDesign::toDisplayName)
             .toList();
Java

こうすると、

「何をしている map なのか」

toDisplayName という名前で一発で分かるし、
同じ変換を別の場所でも再利用できます。

Function を“変換の部品”として設計する

変換を Function として持っておく設計もよく使います。

import java.util.function.Function;

Function<User, String> toName = User::getName;
Function<User, Integer> toAge = User::getAge;
Function<User, String> toNameAndAge =
        u -> u.getName() + " (" + u.getAge() + ")";
Java

これを map に渡すと、

users.stream()
     .map(toNameAndAge)
     .toList();
Java

という形で、「変換の定義」と「パイプラインの流れ」を分離できます。

Function には andThencompose もあり、
「変換を組み合わせて新しい変換を作る」こともできますが、
初心者のうちは「1 つの変換を 1 つの Function」として素直に設計するだけで十分です。


ドメインオブジェクトでの map 設計

例:User リストから「名前だけのリスト」を作る

import java.util.List;

class User {
    private final String name;
    private final int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    String getName() { return name; }
    int getAge() { return age; }
}

public class MapUserExample {
    public static void main(String[] args) {
        List<User> users = List.of(
                new User("Alice", 20),
                new User("Bob", 17),
                new User("Charlie", 25)
        );

        List<String> names =
                users.stream()
                     .map(User::getName)
                     .toList();

        System.out.println(names); // [Alice, Bob, Charlie]
    }
}
Java

ここでの map(User::getName) は、

User を受け取って String(名前)を返す変換」

です。

型の流れを追うと、

Stream<User>map(User::getName)Stream<String>

という変化になっています。

例:User から DTO に変換する

現場でよくあるのが、「エンティティ → DTO」の変換です。

class UserDto {
    final String name;
    final int age;
    UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

private static UserDto toDto(User u) {
    return new UserDto(u.getName(), u.getAge());
}

List<UserDto> dtos =
        users.stream()
             .map(MapUserExample::toDto)
             .toList();
Java

ここでは、

User を受け取って UserDto を返す変換」

map に渡しています。

このように、map は「ドメインオブジェクトの形を変える」場面で非常によく使われます。


map と filter の責務の違いをはっきりさせる

map は「変換」、filter は「選別」

よくある混乱が、

「map の中で条件分岐して null を返す」
「filter の中で値を変えてしまう」

といった“責務の混在”です。

設計としては、

filter で「通すか落とすか」を決める
map で「形を変える」

と役割を分けた方が、パイプラインが読みやすくなります。

例えば、「20 歳以上のユーザーの名前リスト」を作るなら、

List<String> names =
        users.stream()
             .filter(u -> u.getAge() >= 20)   // 選別
             .map(User::getName)              // 変換
             .toList();
Java

というふうに、「どのステップが何をしているか」が一目で分かるように書くのが理想です。


map 設計で意識したい“副作用”の扱い

map の中で副作用を書かない

map は本来、「純粋な変換」に徹するべき場所です。

ここに「ログを書き込む」「外部サービスを呼ぶ」「状態を変更する」などの副作用を入れ始めると、
パイプラインの意味が一気に分かりにくくなります。

副作用を入れたいなら、

途中で値を覗くだけなら peek
最終的な処理なら forEach

に責務を分ける方がきれいです。

users.stream()
     .map(User::getName)
     .peek(name -> System.out.println("name = " + name))
     .toList();
Java

map は「入力 → 出力」の変換だけ、
副作用は別ステップ――この線引きを意識しておくと、
Stream の設計がかなりスッキリします。


map と flatMap の違いを軽く触れておく

「1 対 1 の変換」か、「1 対 多の変換」か

map は「1 要素 → 1 要素」の変換です。

一方、flatMap は「1 要素 → 0 個以上の要素(Stream)」を返し、
それを“平らにして” 1 本のストリームにします。

例えば、「文章のリスト → 単語のリスト」のような変換は flatMap の出番ですが、
初心者のうちはまず「1 対 1 の変換は map」と覚えておけば十分です。


まとめ:map の設計を自分の言葉で定義する

map をあなたの言葉でまとめるなら、

T -> R な変換処理(Function)を、ストリームの全要素に適用して、R 型の新しいストリームを作る中間操作」 です。

特に意識しておきたい設計ポイントは、

変換は「その場のラムダ」から始めて、複雑になったらメソッドや Function に切り出すこと
map の責務は「形を変えること」であり、「選別」は filter、「副作用」は peek / forEach に任せること
ドメインオブジェクトの変換(エンティティ → DTO、オブジェクト → プロパティ)で積極的に使うこと
「1 対 1 の変換」が map、「1 対 多」は flatMap というざっくりした使い分けを持っておくこと

です。

タイトルとURLをコピーしました