JavaのOptionalにおけるorElseとorElseGetの実行タイミングを解説する記事のアイキャッチ。

Java

JavaのOptionalでorElseが実行される理由|orElseGetとの違い

AIによる要約

OptionalのorElseは「値がなければ引数を使う」メソッドですが、引数に書いたメソッド呼び出しはorElseへ渡す前に評価されます。そのためOptionalに値があっても、DB検索や保存処理は実行されます。固定値や生成済みの軽い値ならorElse、値がない場合だけ実行したい処理はorElseGet、未存在を異常として扱うならorElseThrowを使います。違いは戻り値ではなく、処理の実行タイミングで判断します。

新人SE
新人SE
Optionalに値が入っているのに、orElseへ書いたDB検索が動きました。Optionalが壊れているんですか?
ポンコツSE
ポンコツSE
OptionalではなくJavaの引数評価のルールです。orElseを呼ぶ前に、引数のメソッド呼び出しが先に実行されています。

`orElse`と`orElseGet`は、値がない場合の代替値を決めるAPIです。最終的な戻り値だけを見ると同じに見えます。しかし、DBアクセス、外部API、採番、保存、ログなどを渡した瞬間に、実行タイミングの違いが不具合や性能劣化につながります。

特にSpring Data JPAのRepository戻り値をOptionalで受ける現場では、既存データが見つかっているのにINSERTが走る、毎回不要なSELECTが増える、といった問題を起こしやすいポイントです。

この記事のポイント

  • orElseの引数はOptionalに値があっても評価される
  • orElseGetのSupplierは値がない場合だけ実行される
  • 固定値はorElse、処理を伴う代替値はorElseGetが基本
  • 未存在をエラーにするならorElseThrowを使う
  • DBアクセスと副作用は呼び出し回数までテストする

XのJavaクイズで実行順を確認する

Optional.ofでAを保持し、orElseの引数にDBと表示するloadメソッドを渡したJavaコードの出力を問うクイズ画像。
OptionalにはAがあります。DBという文字は出力されるでしょうか。
Optional<String> value = Optional.of("A");

String result = value.orElse(load());
System.out.println(result);

static String load() {
    System.out.println("DB");
    return "B";
}

出力は`DB`、`A`の順です。Optionalが保持するAが最終結果になりますが、`load()`は先に実行されています。

orElseではOptionalに値があってもloadが実行され、DBとAが出力されることを説明する回答画像。
必要なときだけloadを実行するならorElseGetへSupplierを渡します。

元のクイズはXの出題ポスト回答ポストでも確認できます。

orElseが実行されるのはJavaが引数を先に評価するから

Javaではメソッドを呼び出す前に、渡す引数を評価します。次の2行は同じ順序で動くと考えると分かりやすくなります。

// 1行で書いた形
String result = value.orElse(load());

// 実行順を分けて考えた形
String fallback = load();
String result = value.orElse(fallback);

`orElse`自身がloadを呼んでいるわけではありません。`load()`を実行して戻り値Bを作った後、そのBをorElseへ渡しています。Optionalに値があるか確認するのは、その後です。

一方、orElseGetは値ではなく`Supplier`を受け取ります。Supplierの処理本体を今すぐ実行せず、Optionalが空だった場合に呼び出せます。

Optional<String> value = Optional.of("A");

String result = value.orElseGet(() -> load());
System.out.println(result);

この場合はAだけが出力され、DBは表示されません。メソッド参照で`value.orElseGet(Sample::load)`と書いても同じです。

orElseとorElseGetの使い分け

状況選択理由
固定文字列、定数、既にある値orElse評価コストと副作用がほぼない
オブジェクト生成、計算、DB検索orElseGet空の場合だけ処理したい
未存在を異常とするorElseThrow代替値で隠さず例外にする
nullを返す既存仕様orElse(null)境界での互換用。内部へ広げない
String label = optionalName.orElse("未設定");

User user = cachedUser.orElseGet(
        () -> userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new));

Order order = orderRepository.findById(id)
        .orElseThrow(() -> new OrderNotFoundException(id));

「orElseGetの方が常に上位」という話ではありません。`orElse("未設定")`のような固定値までSupplierに包むと、コードが長くなるだけです。代替値を作るために処理が必要かで選びます。

現場で危険なのは副作用を持つ処理

Customer customer = customerRepository.findByEmail(email)
        .orElse(customerRepository.save(new Customer(email)));

このコードは、既存顧客が見つかっていても`new Customer(email)`を作り、`save`を呼びます。ユニーク制約違反、不要なINSERT、監査項目の更新、イベント発行などを起こす可能性があります。

Customer customer = customerRepository.findByEmail(email)
        .orElseGet(() ->
                customerRepository.save(new Customer(email)));

orElseGetなら、検索結果が空の場合だけsaveが実行されます。ただし同時実行がある登録処理では、検索してから保存する間に別トランザクションが同じメールアドレスを登録する可能性があります。orElseGetへ変えるだけで排他や一意性の問題まで解決するわけではありません。

副作用を「代替値」の中へ隠さない

登録、メール送信、採番、外部API呼び出しなどは、単なる値の生成ではありません。処理の意図が重要なら、if文で存在有無を分けた方がレビューしやすい場合があります。

if文の方が読みやすい場合もある

Optional<Customer> found = customerRepository.findByEmail(email);
if (found.isPresent()) {
    return found.get();
}

Customer created = new Customer(email);
auditService.recordCustomerCreation(created);
return customerRepository.save(created);

代替処理が複数行になり、監査や通知も伴うなら、orElseGetへ詰め込むより分岐を明示した方が意図を追いやすくなります。Optionalを使う目的はif文を消すことではなく、値がない可能性を型で表すことです。

orElseThrowは未存在を異常として扱う

注文詳細画面で指定IDが存在しない、更新対象が削除済み、といったケースでは代替オブジェクトを作らず例外にします。

Order order = orderRepository.findById(orderId)
        .orElseThrow(() ->
                new OrderNotFoundException(orderId));

ここでもSupplierが使われるため、`OrderNotFoundException`はOptionalが空の場合だけ生成されます。検索結果がないことが正常なのか異常なのかは、Repositoryではなく呼び出し側のユースケースで決めます。

性能問題は1回ではなく積み重なって出る

不要なDB検索が1回増えても、ローカル環境では気づきにくいものです。しかし一覧100件のループ内でorElseを使えば、Optionalに値があるケースでも100回検索されます。外部APIなら待ち時間と課金、DBなら接続数と負荷へ影響します。

for (Order order : orders) {
    String displayName = Optional.ofNullable(order.getDisplayName())
            .orElse(masterRepository.findName(order.getCode()));
    csv.write(displayName);
}

`orElseGet(() -> masterRepository.findName(...))`へ変えるだけで不要呼び出しは避けられます。ただし値がない注文が多数ある場合はN+1問題が残るため、一括取得やJOINも検討します。

呼び出し回数をテストする

@Test
void 値がある時は代替検索を呼ばない() {
    Optional<String> cached = Optional.of("cache");
    Supplier<String> loader = mock(Supplier.class);

    String actual = cached.orElseGet(loader);

    assertEquals("cache", actual);
    verifyNoInteractions(loader);
}

@Test
void 空の時だけ代替検索を呼ぶ() {
    Optional<String> cached = Optional.empty();
    Supplier<String> loader = mock(Supplier.class);
    when(loader.get()).thenReturn("db");

    String actual = cached.orElseGet(loader);

    assertEquals("db", actual);
    verify(loader).get();
}

戻り値だけをassertすると、orElseでもテストが通る場合があります。副作用や性能が問題になる処理では、RepositoryやSupplierが呼ばれていないことまで確認します。

現場レビューでよくある指摘

// レビューコメント例
orElseの引数にRepository呼び出しがあるため、
Optionalに値がある場合もSQLが実行されます。orElseGetを検討してください。

// レビューコメント例
代替処理の中で保存と通知を行っており、式だけでは副作用が分かりません。
存在有無をifで分け、処理意図を明示したいです。

// レビューコメント例
このユースケースでは未存在を既定値で隠さず、
orElseThrowで業務例外へ変換してください。

提出前のセルフチェック

  • orElseの引数にメソッド呼び出しがないか
  • 代替値の生成にDBや外部APIを使っていないか
  • 保存・採番・通知などの副作用がないか
  • 固定値ならorElseで簡潔に書けないか
  • 未存在を異常にすべきならorElseThrowを使ったか
  • 複雑な代替処理を無理にラムダへ詰めていないか
  • 戻り値だけでなく呼び出し回数をテストしたか
  • ループ内の不要アクセスやN+1がないか

JavaのOptionalを基礎から整理する参考書

Optional、ラムダ式、Supplierをまとめて学ぶと、API名の暗記ではなく評価タイミングから判断できるようになります。

書籍「スッキリわかるJava入門 第5版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

Java文法とオブジェクト指向を、最初から整理する一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

orElseはOptionalの値を確認する前に、Javaが引数を評価します。そのため、値が存在しても引数に書いたDB検索や保存処理は実行されます。

固定値ならorElse、空の場合だけ実行したい処理ならorElseGet、未存在を異常にするならorElseThrowが基本です。戻り値だけでなく、呼び出し回数と副作用まで確認して使い分けましょう。

-Java
-, , ,