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 には andThen や compose もあり、
「変換を組み合わせて新しい変換を作る」こともできますが、
初心者のうちは「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();
Javamap は「入力 → 出力」の変換だけ、
副作用は別ステップ――この線引きを意識しておくと、
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 というざっくりした使い分けを持っておくこと
です。
