Article

☕ JavaのorElseとorElseGetの違い:不要な処理と副作用を避ける

java, backend

JavaのOptionalには、値がない場合の代替値を返すためのorElseとorElseGetがあります。

見た目は似ていますが、実務では代替値を作る処理がいつ評価されるかが重要です。

結論

  • 代替値が定数・安価な生成なら orElse
  • DBアクセス、外部API、重い計算、副作用を含む処理なら orElseGet

ポイントは、orElseの引数は通常のJavaメソッド引数なので、Optionalに値が入っていても先に評価されることです。

orElseは値があっても代替値を作る

Optional<String> name = Optional.of("Alice");

String result = name.orElse(loadDefaultName());

この場合、resultは"Alice"ですが、loadDefaultName()自体は呼ばれます。

private static String loadDefaultName() {
    System.out.println("default loaded");
    return "Guest";
}

出力は次のようになります。

default loaded

値が存在するのに、代替値生成処理は実行されています。

orElseGetは必要なときだけ実行する

同じ処理をorElseGetにすると、Supplierは値がないときだけ評価されます。

Optional<String> name = Optional.of("Alice");

String result = name.orElseGet(() -> loadDefaultName());

この場合、loadDefaultName()は呼ばれません。

Optional.empty()のときだけ実行されます。

Optional<String> name = Optional.empty();

String result = name.orElseGet(() -> loadDefaultName());

DBアクセスをorElseへ入れると危険

例えばキャッシュからユーザーを探し、無ければDBから取得する処理を考えます。

User user = cache.find(userId)
        .orElse(repository.findById(userId));

この書き方では、キャッシュにユーザーがいてもrepository.findById(userId)が先に実行されます。

つまり、本来不要なDBアクセスが毎回発生する可能性があります。

遅延実行したい場合はorElseGetを使います。

User user = cache.find(userId)
        .orElseGet(() -> repository.findById(userId));

外部APIやログなどの副作用にも注意

代替値生成が単なる計算ではなく、副作用を持つ場合はさらに注意が必要です。

String token = cachedToken
        .orElse(issueNewToken());

issueNewToken()が新しいトークンを発行する処理なら、キャッシュ済みトークンが存在していても発行処理が走ります。

この場合もorElseGetが適しています。

String token = cachedToken
        .orElseGet(() -> issueNewToken());

定数ならorElseで十分

一方、次のような固定値ならorElseで問題ありません。

String displayName = optionalName.orElse("Guest");

"Guest"を用意するコストはほぼ無視でき、副作用もありません。

読みやすさの面でもこのケースで無理にorElseGet(() -> "Guest")とする必要はありません。

メソッド参照も使える

引数のないメソッドなら、ラムダの代わりにメソッド参照も使えます。

String value = optional.orElseGet(this::loadDefault);
private String loadDefault() {
    return "default";
}

nullを返すSupplierは避ける

orElseGetのSupplierは型としてはnullを返すこともできます。

ただし、Optionalを使っているのに最終結果でnullを戻すと、呼び出し側のnull安全性が分かりにくくなります。

String value = optional.orElseGet(() -> null);

特別な理由がなければ、明示的なデフォルト値を返すか、値が必須ならorElseThrowを検討した方が意図が伝わりやすくなります。

String value = optional.orElseThrow(
        () -> new IllegalStateException("value is required")
);

性能差だけの問題ではない

orElseとorElseGetの違いは「どちらが高速か」だけではありません。

代替値生成処理に次のようなものが含まれると、動作そのものが変わります。

  • DB更新
  • 外部API呼び出し
  • ファイル生成
  • ID採番
  • メール送信
  • ログや監査イベントの記録

そのため、代替値生成処理に副作用があるかも判断材料にします。

使い分け

ケース 推奨
文字列や定数を返す orElse
軽い値生成 orElseでもよい
DBアクセス orElseGet
外部API orElseGet
重い計算 orElseGet
副作用のある処理 orElseGet
値が無いこと自体が異常 orElseThrow

まとめ

orElseとorElseGetの違いは、代替値の評価タイミングです。

  • orElse(other): otherは通常の引数として先に評価される
  • orElseGet(supplier): 値がないときだけSupplierを実行する
  • 重い処理や副作用を含む場合はorElseGet
  • 単純な定数ならorElseで十分
  • 値が必須ならorElseThrowも検討する

Optionalを使うときは「値があるかどうか」だけでなく、代替値を作る処理がいつ実行されるかまで見ると、不要な処理や意図しない副作用を避けやすくなります。

参考