Optional.flatMap を一言でいうと
Optional.flatMap は
「中身が“あるときだけ”、Optional を返す関数を適用し、その二重になった Optional を平らにして返す」
ためのメソッドです。
もっと感覚的に言うと、
mapは「中身を普通の値に変換する」flatMapは「中身を Optional に変換する」
ときに使うものです。
Optional<Optional<T>> みたいな「Optional の中に Optional」が出てきそうな場面で、それをきれいに 1 段にしてくれます。
まず map と flatMap の違いを型で感じる
map の型の動き
map のシグネチャをざっくり書くとこうです。
<T, U> Optional<U> map(Function<? super T, ? extends U> mapper)
Java「T を U に変える関数」を受け取って、Optional<U> を返します。
例えば、
Optional<String> maybeText = Optional.of("hello");
Optional<Integer> maybeLength =
maybeText.map(String::length); // String -> Integer
Javaここでは String::length が String -> Integer なので、結果は Optional<Integer> です。
flatMap の型の動き
flatMap のシグネチャはこうです。
<T, U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)
Java「T を Optional<U> に変える関数」を受け取って、Optional<U> を返します。
例えば、
Optional<User> maybeUser = findUser();
Optional<Address> maybeAddress =
maybeUser.flatMap(User::getAddressOptional); // User -> Optional<Address>
Javaここで User::getAddressOptional は User -> Optional<Address> です。
普通に map を使うと Optional<Optional<Address>> になってしまうところを、flatMap が 1 段にしてくれます。
具体例1:User → Optional<Address> のチェーン
モデルクラスを用意する
まず、こんなモデルを考えます。
class Address {
private final String zipCode;
Address(String zipCode) { this.zipCode = zipCode; }
String getZipCode() { return zipCode; }
}
class User {
private final Optional<Address> address; // 住所がないユーザーもいる
User(Optional<Address> address) { this.address = address; }
Optional<Address> getAddressOptional() { return address; }
}
JavaUser は「住所があるかもしれないし、ないかもしれない」ので、Optional<Address> を返すメソッドを持っています。
map を使うと Optional<Optional<…>> になる
ユーザーも「いるかもしれないし、いないかもしれない」とします。
Optional<User> maybeUser = findUser(); // 見つからなければ Optional.empty()
Javaここから「郵便番号(String)」までたどり着きたいとき、素直に map を使うとこうなります。
Optional<Optional<Address>> nested =
maybeUser.map(User::getAddressOptional);
Java型が Optional<Optional<Address>> になってしまいました。
さらに map(Address::getZipCode) しようとすると、
nested.map(optAddr -> optAddr.map(Address::getZipCode));
Javaみたいに、どんどんネストが深くなっていきます。
flatMap で一気に平らにする
ここで flatMap の出番です。
Optional<String> maybeZip =
maybeUser
.flatMap(User::getAddressOptional) // Optional<Address>
.map(Address::getZipCode); // Optional<String>
Java流れを言葉にすると、
- ユーザーがいれば
getAddressOptional()を呼び、その結果のOptional<Address>をそのまま「中身」として扱う - ユーザーがいなければ、その時点で
Optional.empty() - そのあと、住所があれば
getZipCode()を呼び、なければやはり空
という動きになります。
flatMap がやっているのは、
「Optional<Optional<Address>> になりそうなところを、Optional<Address> に平らにする」
という一点です。
具体例2:Optional を返す関数をつなげる
「バリデーション → 変換 → 検索」みたいな流れ
例えば、こんな関数があるとします。
Optional<String> normalizeEmail(String raw); // 入力を整形して、ダメなら empty
Optional<User> findByEmail(String email); // 見つからなければ empty
Optional<Order> findLatestOrder(User user); // 注文がなければ empty
Javaこれらを組み合わせて、
「生の文字列から、最新の注文を Optional で返す」
という処理を書きたい。
if / isPresent / get で書くとどうなるか
素朴に書くと、こうなりがちです。
Optional<String> maybeEmail = normalizeEmail(raw);
if (maybeEmail.isEmpty()) {
return Optional.empty();
}
Optional<User> maybeUser = findByEmail(maybeEmail.get());
if (maybeUser.isEmpty()) {
return Optional.empty();
}
Optional<Order> maybeOrder = findLatestOrder(maybeUser.get());
return maybeOrder;
JavaisEmpty() と get() のコンボが 3 回出てきて、かなりゴツいです。
flatMap でつなげると一気にスッキリする
同じことを flatMap で書くと、こうなります。
Optional<Order> result =
normalizeEmail(raw)
.flatMap(this::findByEmail)
.flatMap(this::findLatestOrder);
Javaここでやっていることは、
- 正常なら次のステップに進む
- どこかで empty になったら、そのまま最後まで empty
という「早期リターン」の連鎖です。
flatMap は、「Optional を返す関数を、if / get を書かずに安全につなげる」ための道具、と捉えるとしっくりきます。
map と flatMap の使い分けを感覚で覚える
ルールはシンプル
関数が「普通の値」を返すなら map。
Optional<User> maybeUser = findUser();
Optional<String> maybeName =
maybeUser.map(User::getName); // User -> String
Java関数が「Optional を返す」なら flatMap。
Optional<User> maybeUser = findUser();
Optional<Address> maybeAddress =
maybeUser.flatMap(User::getAddressOptional); // User -> Optional<Address>
Javaこれだけです。
「Optional の中に Optional が見えたら flatMap を疑う」
コードを書いていて、
Optional<Optional<T>>Optional<Optional<Optional<T>>>
のような型が出てきたら、ほぼ確実に「flatMap を使うべき場所」があります。
mapで Optional を返す関数を適用していないか- その結果をさらに
mapしてネストを増やしていないか
を一度立ち止まって見直してみてください。
Optional.flatMap を使うときの設計ポイント
isPresent / get コンボから卒業する
Optional を使い始めたときに一番やりがちなのが、
if (maybeX.isPresent()) {
Y y = f(maybeX.get());
...
}
Javaという書き方です。
flatMap を使うと、
- 「中身があるときだけ次に進む」
- 「どこかで empty になったら、そのまま empty」
という流れを、if 文なしで表現できます。
Optional を返す関数が 2 個以上つながるときは、
「isPresent / get で書いていないか?」
「flatMap でつなげられないか?」
を毎回自分に問いかけると、設計が一気にきれいになります。
map と flatMap を組み合わせる
実際のコードでは、map と flatMap を混ぜて使うことが多いです。
Optional<String> maybeZip =
findUser()
.flatMap(User::getAddressOptional) // Optional<Address>
.map(Address::getZipCode); // Optional<String>
Java「Optional を返すところ」は flatMap、
「普通の値を返すところ」は map。
このリズムが体に入ると、Optional まわりのコードがかなり自然に書けるようになります。
まとめ:Optional.flatMap を自分の言葉で説明するなら
あなたの言葉で Optional.flatMap を説明するなら、こうです。
「Optional.flatMap は、“中身があるときだけ Optional を返す関数を適用し、その二重になった Optional を 1 段にして返す”メソッド。
Optional を返す処理を if / isPresent / get でつなぐ代わりに、flatMap のチェーンとして安全に、読みやすくつなげるための道具。」
