Java | Java 詳細・モダン文法:Optional – Optional.flatMap

Java Java
スポンサーリンク

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::lengthString -> 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::getAddressOptionalUser -> 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; }
}
Java

User は「住所があるかもしれないし、ないかもしれない」ので、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;
Java

isEmpty()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 を組み合わせる

実際のコードでは、mapflatMap を混ぜて使うことが多いです。

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 のチェーンとして安全に、読みやすくつなげるための道具。」

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