Java Tips | コレクション:マッピング

Java Java
スポンサーリンク

マッピングは「一覧の“形”を変える」技

マッピングは、ざっくり言うと
「ある型の一覧を、別の型の一覧に変換する」処理です。

ユーザー一覧から「名前だけの一覧」を作る。
商品一覧から「価格だけの一覧」を作る。
注文一覧から「DTO(画面表示用オブジェクト)の一覧」を作る。

こういう「A の一覧 → B の一覧」の変換を、
安全に・読みやすく・再利用しやすく書くための考え方が“マッピング”です。


一番基本:Stream の map で値を変換する

例:数値一覧を「文字列一覧」に変換する

まずは、型を変える一番シンプルな例から。

import java.util.List;
import java.util.stream.Collectors;

public class MapBasic {

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

        List<String> texts =
                numbers.stream()
                       .map(n -> "No." + n)
                       .collect(Collectors.toList());

        System.out.println(texts); // [No.1, No.2, No.3]
    }
}
Java

ここでの重要ポイントは二つです。

一つ目は、map(n -> "No." + n) が「1 要素を別の形に変換する関数」であることです。
n -> "No." + n は、「Integer を String に変える」変換ルールを表しています。

二つ目は、map は「要素数は変えずに、中身の“形”だけを変える」ということです。
元が 3 件なら、結果も 3 件です(フィルタとはここが違います)。


オブジェクト一覧から「特定のフィールドだけ」を取り出す

例:ユーザー一覧から「名前一覧」を作る

業務では、オブジェクトから特定のフィールドだけを抜き出すマッピングが頻出します。

class User {
    private final String name;
    private final String email;
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    public String getName() { return name; }
    public String getEmail() { return email; }
}
Java

この一覧から「名前だけの List<String>」を作ります。

import java.util.List;
import java.util.stream.Collectors;

public class UserNameMapping {

    public static void main(String[] args) {
        List<User> users = List.of(
                new User("山田", "yamada@example.com"),
                new User("佐藤", "sato@example.com")
        );

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

        System.out.println(names); // [山田, 佐藤]
    }
}
Java

ここでの重要ポイントは、map(User::getName) という書き方です。
「ユーザーから名前を取り出す」というルールを、メソッド参照でそのまま書けています。
User::getName という一語で、「User → String(名前)」という変換を表現できる感覚を持ってほしいです。


マッピングユーティリティにまとめる

「毎回 stream と map と collect を書きたくない」を解消する

同じようなマッピングを何度も書くなら、
ユーティリティメソッドにしてしまうとスッキリします。

import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public final class Mappings {

    private Mappings() {}

    public static <T, R> List<R> map(
            Collection<T> source,
            Function<? super T, ? extends R> mapper
    ) {
        if (source == null || source.isEmpty()) {
            return List.of();
        }
        return source.stream()
                     .map(mapper)
                     .collect(Collectors.toList());
    }
}
Java

使い方はこうです。

List<String> names =
        Mappings.map(users, User::getName);

List<String> emails =
        Mappings.map(users, User::getEmail);
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「Function<? super T, ? extends R> mapper が“1 要素をどう変換するか”を表している」ことです。
User::getName を渡せば「ユーザー → 名前」、User::getEmail を渡せば「ユーザー → メールアドレス」という変換になります。

二つ目は、「ユーティリティ側は“どう一覧を変換するか”だけを知っていて、“何をどう変換するか”は呼び出し側が決める」構造になっていることです。
これにより、同じメソッドでどんな型の変換にも対応できます。

三つ目は、「null や空コレクションの扱いを一箇所に閉じ込めている」ことです。
呼び出し側は「変換したい」という意図だけに集中できます。


DTO へのマッピング:業務で一番よく出るパターン

例:エンティティ → レスポンス DTO

画面表示や API レスポンス用に、
「内部のモデル(エンティティ)から DTO に変換する」マッピングは超頻出です。

class UserEntity {
    private final String name;
    private final String email;
    public UserEntity(String name, String email) {
        this.name = name;
        this.email = email;
    }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

class UserResponse {
    private final String displayName;
    private final String maskedEmail;
    public UserResponse(String displayName, String maskedEmail) {
        this.displayName = displayName;
        this.maskedEmail = maskedEmail;
    }
    public String getDisplayName() { return displayName; }
    public String getMaskedEmail() { return maskedEmail; }
}
Java

エンティティ一覧からレスポンス一覧を作るマッピングはこう書けます。

import java.util.List;
import java.util.stream.Collectors;

public class UserDtoMapping {

    private static UserResponse toResponse(UserEntity e) {
        String masked = e.getEmail().replaceAll("(.).+(@.+)", "$1***$2");
        return new UserResponse(e.getName(), masked);
    }

    public static void main(String[] args) {
        List<UserEntity> entities = List.of(
                new UserEntity("山田", "yamada@example.com"),
                new UserEntity("佐藤", "sato@example.com")
        );

        List<UserResponse> responses =
                entities.stream()
                        .map(UserDtoMapping::toResponse)
                        .collect(Collectors.toList());
    }
}
Java

ここでの重要ポイントは、
「1 要素の変換ロジック(toResponse)をまずきちんと書き、それを map に渡している」ことです。

map(UserDtoMapping::toResponse) という一行が、
「エンティティ一覧をレスポンス一覧に変換する」という業務処理をそのまま表しています。
この「1 要素変換関数を先に定義してから map に渡す」スタイルは、実務でとても読みやすくなります。


null を含むデータのマッピング

「null をそのまま流すか」「null を除外するか」を決める

現実のデータには null が混ざります。
マッピングで大事なのは、「null をどう扱うか」を決めることです。

例えば、「null はそのまま null に変換する」なら、こうです。

public static <T, R> List<R> mapAllowNull(
        Collection<T> source,
        Function<? super T, ? extends R> mapper
) {
    if (source == null || source.isEmpty()) {
        return List.of();
    }
    return source.stream()
                 .map(e -> e == null ? null : mapper.apply(e))
                 .collect(Collectors.toList());
}
Java

逆に、「null 要素はそもそも除外したい」なら、こうです。

public static <T, R> List<R> mapNonNull(
        Collection<T> source,
        Function<? super T, ? extends R> mapper
) {
    if (source == null || source.isEmpty()) {
        return List.of();
    }
    return source.stream()
                 .filter(e -> e != null)
                 .map(mapper)
                 .collect(Collectors.toList());
}
Java

ここでの重要ポイントは、
「null の扱いも“マッピングルールの一部”」だと意識することです。

プロジェクトとして「null は除外する」「null はそのまま流す」など、
方針を決めてユーティリティに閉じ込めておくと、呼び出し側が迷わなくなります。


フィルタとマッピングの順番を意識する

「まず絞る → それから形を変える」が基本

フィルタとマッピングはセットで使うことが多いです。
順番を意識すると、コードの意図がとても読みやすくなります。

例えば、「有効ユーザーだけの名前一覧」を作るなら、こうです。

import java.util.List;
import java.util.stream.Collectors;

List<String> activeNames =
        users.stream()
             .filter(User::isActive)   // まず絞る(フィルタ)
             .map(User::getName)       // それから形を変える(マッピング)
             .collect(Collectors.toList());
Java

ここでの重要ポイントは、
「フィルタは“対象を決める”、マッピングは“形を決める”」という役割分担です。

この順番を守ると、
「誰を対象にして」「何を取り出しているのか」が一目で分かるコードになります。


まとめ:マッピングユーティリティで身につけてほしい感覚

マッピングは、
単に「型を変えるテクニック」ではなく、
「業務で扱うデータの“形”を、目的に合わせて整理し直す技術」です。

stream().map(変換関数) の基本形に慣れる。
Mappings.map(コレクション, 変換関数) のようなユーティリティにして、「1 要素変換」を関数として渡す。
DTO 変換などは「1 要素変換メソッド」を先に定義し、それを map に渡すスタイルにする。
null の扱い(そのまま流すか、除外するか)をユーティリティ側で統一する。
フィルタと組み合わせるときは、「まず絞る → それから形を変える」という流れを意識する。

あなたのコードのどこかに、
for 文で「新しい List を作って、1 件ずつ add しながら変換している」箇所があれば、
それを一度「マッピングユーティリティ+Stream」に置き換えられないか眺めてみてください。

その小さな一歩が、
「データの“形”を自在に操れるエンジニア」への、
確かなステップになります。

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