【プログラミングしてみよう55】JavaのOptionalをStreamと組み合わせる

Java

前回はStream APIの reduce・groupingBy・partitioningBy など高度な集計操作を学びました。

今回は「OptionalとStreamの組み合わせ」について解説します。 以前それぞれ個別に学んだOptionalとStream APIですが、組み合わせることでより安全で読みやすいコードが書けるようになります。

OptionalとStreamを組み合わせる理由

以前学んだ Optional は「値があるかもしれないし、ないかもしれない」を表すクラスで、nullチェックを安全に書けます。Stream APIはコレクションの処理をパイプラインで書けます。

この2つを組み合わせると、たとえば「リストから条件に合う最初の要素を取得し、存在する場合だけ加工して返す」といった処理を、nullチェックやif文なしで安全に書けます。

// nullチェックを使った書き方
User user = findUser(id);
if (user != null) {
    return user.getName().toUpperCase();
}
return "不明";

// OptionalとStreamを組み合わせた書き方
return findUser(id)
        .map(User::getName)
        .map(String::toUpperCase)
        .orElse("不明");

サンプルコード(OptionalのStreamメソッド)

ファイル作成

「Java43」フォルダに OptionalStreamTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

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

public class OptionalStreamTest {
    public static void main(String[] args) {
        // Optional.stream() でOptionalをStreamに変換(Java 9+)
        Optional<String> present = Optional.of("ぱんまる");
        Optional<String> empty   = Optional.empty();

        // 値ありのOptionalはstream()で1要素のStreamになる
        System.out.println("値あり stream:");
        present.stream().forEach(System.out::println);

        // 値なしのOptionalはstream()で空のStreamになる
        System.out.println("値なし stream:");
        empty.stream().forEach(System.out::println);
        System.out.println("(何も出力されません)");

        // OptionalのリストからStream.flatMapで値ありだけ取り出す
        List<Optional<String>> optionals = List.of(
            Optional.of("ぱんまる"),
            Optional.empty(),
            Optional.of("たろう"),
            Optional.empty(),
            Optional.of("はなこ")
        );

        List<String> names = optionals.stream()
                .flatMap(Optional::stream) // 値ありだけ取り出す
                .collect(Collectors.toList());

        System.out.println("\nOptionalリストから値ありだけ: " + names);

        // mapとflatMapの違い
        Optional<Optional<String>> nested = Optional.of(Optional.of("ネスト"));
        Optional<String> mapped    = nested.map(o -> o.orElse("なし"));
        Optional<String> flatMapped = nested.flatMap(o -> o);

        System.out.println("\nmapped: " + mapped);
        System.out.println("flatMapped: " + flatMapped);
    }
}

コンパイル

javac OptionalStreamTest.java

実行

java OptionalStreamTest

実行結果は次のようになります。

値あり stream:
ぱんまる
値なし stream:
(何も出力されません)

Optionalリストから値ありだけ: [ぱんまる, たろう, はなこ]

mapped: Optional[ネスト]
flatMapped: Optional[ネスト]

サンプルコード(実践的な組み合わせ例)

ファイル作成

「Java43」フォルダに OptionalStreamPractice.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

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

public class OptionalStreamPractice {
    record User(int id, String name, String email) {}
    record Order(int userId, String product, int price) {}

    static List<User> users = List.of(
        new User(1, "ぱんまる", "panmaru@example.com"),
        new User(2, "たろう",   "taro@example.com"),
        new User(3, "はなこ",   "hanako@example.com")
    );

    static List<Order> orders = List.of(
        new Order(1, "コーヒー",   350),
        new Order(1, "ケーキ",     500),
        new Order(2, "お茶",       150),
        new Order(4, "ジュース",   200) // userId=4は存在しない
    );

    // IDからユーザーを検索(存在しない場合はOptional.empty())
    static Optional<User> findUser(int id) {
        return users.stream()
                    .filter(u -> u.id() == id)
                    .findFirst();
    }

    public static void main(String[] args) {
        // ユーザー検索とメールアドレス取得
        System.out.println("--- ユーザー検索 ---");
        for (int id : new int[]{1, 2, 99}) {
            String email = findUser(id)
                    .map(User::email)
                    .orElse("ユーザーが見つかりません");
            System.out.println("ID " + id + ": " + email);
        }

        // 注文ユーザーの名前一覧(存在するユーザーのみ)
        System.out.println("\n--- 注文ユーザー名 ---");
        List<String> orderUserNames = orders.stream()
                .map(order -> findUser(order.userId()))
                .flatMap(Optional::stream)          // 存在するユーザーだけ
                .map(User::name)
                .distinct()                          // 重複排除
                .collect(Collectors.toList());
        System.out.println(orderUserNames);

        // ユーザーIDごとの注文合計金額
        System.out.println("\n--- ユーザー別注文合計 ---");
        Map<Integer, Integer> totalByUser = orders.stream()
                .collect(Collectors.groupingBy(
                        Order::userId,
                        Collectors.summingInt(Order::price)));

        totalByUser.forEach((userId, total) -> {
            String userName = findUser(userId)
                    .map(User::name)
                    .orElse("不明なユーザー");
            System.out.println(userName + ": ¥" + total);
        });

        // 最も高い注文金額とその商品
        System.out.println("\n--- 最高金額の注文 ---");
        orders.stream()
              .max(java.util.Comparator.comparingInt(Order::price))
              .ifPresent(order -> {
                  String userName = findUser(order.userId())
                          .map(User::name)
                          .orElse("不明");
                  System.out.println(userName + "の注文「"
                          + order.product() + "」: ¥" + order.price());
              });
    }
}

コンパイル

javac OptionalStreamPractice.java

実行

java OptionalStreamPractice

実行結果は次のようになります。

--- ユーザー検索 ---
ID 1: panmaru@example.com
ID 2: taro@example.com
ID 99: ユーザーが見つかりません

--- 注文ユーザー名 ---
[ぱんまる, たろう]

--- ユーザー別注文合計 ---
ぱんまる: ¥850
たろう: ¥150
不明なユーザー: ¥200

--- 最高金額の注文 ---
ぱんまるの注文「ケーキ」: ¥500

コードの解説

Optional.stream() はJava 9から追加されたメソッドで、値がある場合は1要素のStream、値がない場合は空のStreamを返します。これにより flatMap(Optional::stream) で「Optionalのリストから値ありの要素だけ取り出す」操作がシンプルに書けます。

stream().filter(...).findFirst() はStreamの中から条件に合う最初の要素を Optional で返します。見つからなければ Optional.empty() が返るため、nullを返さなくて済みます。

.map(order -> findUser(order.userId())).flatMap(Optional::stream) は「注文からユーザーIDで検索し、存在するユーザーだけを取り出す」パターンです。map だと Stream<Optional<User>> になりますが、flatMap(Optional::stream) で Stream<User> に変換できます。

ifPresent() は値がある場合だけ処理を実行するため、最高金額の注文が存在する場合だけ出力します。Optional・Stream・Comparator・findUserの結果を組み合わせることで、nullチェックなしで安全にデータを扱えています。

まとめ

OptionalとStreamを組み合わせることで、nullチェックやif文を使わずに安全でシンプルなデータ処理が書けます。Optional.stream() でOptionalをStreamに変換し、flatMap(Optional::stream) でOptionalのリストから値ありだけ取り出すパターンは特によく使われます。findFirst() でStreamの検索結果をOptionalで受け取り、.map() や .orElse() でチェーンして処理する書き方を身につけることで、より洗練されたJavaコードが書けるようになります。

2026/08/03(月) ぱんまる

コメント

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