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

Java Java
スポンサーリンク

Optional と Stream を一緒に考える理由

Java でモダンなコードを書くとき、OptionalStream はセットで設計を考えると一気にきれいになります。
どちらも「要素があるかもしれないし、ないかもしれない」という状況を、null に頼らずに表現するための仕組みだからです。

Stream は「0 個以上の要素の流れ」、Optional は「0 個か 1 個の要素の箱」と捉えると、役割の違いとつながりが見えやすくなります。


Optional は「0 個か 1 個の Stream」として考える

findFirst / findAny の戻り値が Optional である意味

Stream#findFirstfindAny の戻り値が Optional<T> なのは、「見つからないかもしれない 1 件」を表現したいからです。

Optional<User> maybeUser =
        users.stream()
             .filter(u -> u.getId() == 10)
             .findFirst();
Java

ここで maybeUser は、「ユーザーが 1 人見つかるかもしれないし、誰も見つからないかもしれない」という状態を持っています。
昔なら User を返して、見つからなければ null を返す、という設計になりがちでしたが、それだと呼び出し側が null チェックを忘れた瞬間に NPE になります。

Optional を返すことで、「見つからない可能性がある」という事実を型に埋め込めるのが、設計上の大きなポイントです。

Optional を「小さな Stream」と見なす感覚

Optional は、概念的には「要素数が 0 か 1 の Stream」として考えることができます。

要素があれば 1 個だけ流れてくる。
なければ何も流れてこない。

このイメージを持っておくと、Optional に対して mapflatMap を使う感覚が、Stream と自然につながります。


Optional と Stream の map / flatMap の対応

Optional#map と Stream#map

Optional#map は、「中身があれば変換し、なければ何もしない」という動きをします。

Optional<User> maybeUser = findUserById(10);

Optional<String> maybeName =
        maybeUser.map(User::getName);
Java

Stream#map も、「要素があれば変換し、なければ何もしない」という意味では同じです。

Stream<User> userStream = ...;

Stream<String> names =
        userStream.map(User::getName);
Java

どちらも「要素の有無を気にせず、“あるものだけ”変換する」というスタイルで書けるのが共通点です。

Optional#flatMap と Stream#flatMap

flatMap も同じように対応しています。

Optional#flatMap は、「中身があれば Optional を返す関数を適用し、その Optional を平らにする」という動きです。

Optional<User> maybeUser = findUserById(10);

Optional<Address> maybeAddress =
        maybeUser.flatMap(User::getAddressOptional);
Java

Stream#flatMap は、「要素 1 つを 0 個以上の要素の Stream に変換し、それらを 1 本の Stream に平らにする」という動きです。

Stream<User> users = ...;

Stream<Address> addresses =
        users.flatMap(u -> u.getAddresses().stream());
Java

どちらも、「ネストしたコンテナ(Optional の中の Optional、Stream の中の Stream)を 1 段にする」という役割を持っています。


Optional と Stream をどうつなぐか

Stream から Optional へ:終端操作で 1 件に絞る

Stream から Optional へは、findFirst / findAny / max / min などの終端操作で自然につながります。

Optional<Integer> max =
        numbers.stream()
               .filter(n -> n > 0)
               .max(Integer::compareTo);
Java

ここで max は、「条件を満たす要素が 1 つもなければ空」という状態を持つ Optional<Integer> です。

「複数かもしれないもの(Stream)」から「0 か 1 個(Optional)」へ縮約する、というイメージです。

Optional から Stream へ:stream() で「0 か 1 要素の Stream」にする

Java 9 以降、Optional には stream() メソッドがあります。

Optional<String> maybeName = findName();

maybeName.stream()
         .map(String::toUpperCase)
         .forEach(System.out::println);
Java

中身があれば 1 要素の Stream、なければ空の Stream として扱えるので、
「Optional を他の Stream パイプラインに自然に混ぜる」ことができます。

例えば、「ユーザー ID のリストから、存在するユーザーだけを集める」処理はこう書けます。

List<Integer> ids = List.of(1, 2, 3, 4);

List<User> users =
        ids.stream()
           .map(id -> findUserById(id)) // Optional<User>
           .flatMap(Optional::stream)   // Optional を 0/1 要素の Stream として平らにする
           .toList();
Java

flatMap(Optional::stream) が、「見つからなかった ID(空の Optional)」を自然にスキップしてくれるのがポイントです。


設計のポイント1:Optional を「戻り値専用」にする

フィールドや引数に Optional を使わない

Optional は「戻り値として“値がないかもしれない”ことを表現する」ためのクラスとして設計されています。
フィールドやメソッド引数に使うと、かえってコードが読みにくくなったり、意図しない null と Optional の二重管理になったりします。

良い例は「検索メソッドの戻り値」です。

Optional<User> findById(int id) { ... }
Java

悪い例は「エンティティのフィールドに Optional を持たせる」ような設計です。

class User {
    Optional<Address> address; // これはやめた方がいい
}
Java

この場合は、単に Address フィールドを null 許容にして、外側の API で Optional に包む方がまだマシです。


設計のポイント2:Optional と Stream の「出口」を意識する

「ここで Optional にする」「ここで Stream を終わらせる」を決める

設計で大事なのは、「どこまで Stream でつなぎ、どこで Optional や単純な値に落とすか」を意識的に決めることです。

例えば、サービス層のメソッド設計を考えるとき、こういう選択肢があります。

ユーザー一覧を返すメソッドは List<User>Stream<User> を返す。
ユーザー 1 件を返すメソッドは Optional<User> を返す。

そして呼び出し側では、

Stream のままなら map / filter / collect でパイプラインを組む。
Optional なら map / flatMap / orElse / orElseThrow で扱い方を決める。

この「レイヤーごとにどの抽象(Stream / Optional / 生の値)を使うか」を揃えると、コード全体の一貫性が出てきます。


設計のポイント3:null と Optional と Stream を混在させない

「null を返さない」「Optional を unwrap しすぎない」

モダンな設計では、次のようなルールを置くとスッキリします。

外向きの API では、null を返さない。
「ないかもしれない 1 件」は Optional で返す。
「0 個以上」は StreamList で返す。

そして、Optional を受け取った側は、できるだけ早い段階で map / flatMap / orElse などで扱い方を決めてしまい、get() で無理やり中身を取り出さないようにします。

Stream についても同じで、「途中で collect して List にしてからまた Stream にする」といった無駄な変換は避け、
「どこで終端操作を打つか」を意識して設計するのが大事です。


まとめ:Optional と Stream を自分の言葉で整理する

最後に、あなたの言葉で整理するとこうなります。

Stream は「0 個以上の要素の流れ」、Optional は「0 個か 1 個の要素の箱」。
findFirst などで Stream から Optional に縮約し、Optional#stream で Optional を 0/1 要素の Stream として扱える。
map / flatMap の考え方は Optional と Stream で共通していて、「あるものだけ変換する」「ネストを平らにする」という発想で書ける。
設計としては、「1 件かもしれないものは Optional」「複数かもしれないものは Stream / List」「null は返さない」というルールを決めると、コードが一気に安全で読みやすくなる。

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