AIによる要約
Optional.getは値が存在するときだけ使え、emptyならNoSuchElementExceptionです。直前にisPresentを確認すれば動きますが、値の取り出しと分岐が離れると読みづらくなります。必須ならorElseThrowで業務例外へ、既定値ならorElse・orElseGet、変換ならmap、存在時だけ処理するならifPresentを使います。Optionalを使っても、0件を正常・404・業務エラーのどれにするかは仕様として決める必要があります。


Optionalは値がない可能性を型で表しますが、無条件にget()を呼ぶと、その情報を無視しているのと同じです。テストデータに対象IDがある間は動き、削除済みや不正IDで初めてNoSuchElementExceptionになります。
重要なのはgetを別メソッドへ置き換えることだけではありません。検索0件を画面の404、業務上の未登録、処理対象なし、システム異常のどれにするか決め、適切な例外や戻り値へ変換します。
この記事のポイント
- Optional.getはemptyでNoSuchElementExceptionになる
- 必須値はorElseThrowで例外の意味を明示する
- 既定値はorElseとorElseGetを使い分ける
- 値の変換はmapでつなげられる
- 0件時の扱いはRepositoryではなく利用側の仕様で決める
getは値の存在を保証しない
Optional.getは、値がある場合にその値を返します。emptyの場合はNoSuchElementException: No value presentです。戻り値がOptionalである時点で、呼び出し側に0件対応が必要だと示されています。
Optional<User> result = repository.findById(id);
User user = result.get(); // emptyならNoSuchElementExceptiongetを使った例外は、どのIDが見つからなかったか、業務上どう扱うべきかを表しません。低レベル例外が共通ハンドラで500になると、単なる未存在がシステム障害に見えます。
必須ならorElseThrowで業務例外へ変換する
対象が必ず必要な処理では、見つからない場合の例外を明示します。例外へIDを持たせるとログやエラーレスポンスを組み立てやすくなります。
User user = repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));例外生成は値がない場合だけ実行されます。Controllerで404へ変換するのか、Serviceで業務エラーにするのか、既存の例外設計へ合わせます。
isPresentとgetを重ねすぎない
次の書き方は安全に見えますが、Optionalを通常のnullチェックと同じ形で扱い、処理が長くなります。
Optional<User> optional = repository.findById(id);
if (optional.isPresent()) {
User user = optional.get();
sendMail(user.getEmail());
}存在時だけ処理するならifPresent、値を変換して返すならmap、必須ならorElseThrowの方が意図を表せることがあります。ただし複雑な業務分岐を無理にチェーンへ詰めず、読みやすさを優先します。
存在時だけ処理するifPresent
対象がなければ何もしないことが仕様なら、ifPresentで表せます。たとえば任意のキャッシュ削除や補助通知です。
repository.findById(id)
.ifPresent(user -> notificationService.send(user));注文取消対象がないのに何もしない、という処理は利用者へ成功と見える可能性があります。無視が正しい場面に限定します。
mapで値を変換する
Optionalの中身から別の値を取り出すならmapを使えます。途中でnullを返すとemptyになるため、nullが混ざる設計も確認します。
String email = repository.findById(id)
.map(User::getEmail)
.orElseThrow(() -> new EmailNotFoundException(id));ユーザー未存在とメール未設定を同じEmailNotFoundExceptionにしてよいかは別問題です。原因を分ける必要があるなら、段階を分けて明示します。
orElseとorElseGetの違い
既定値を返す場合、値があってもorElseの引数は先に評価されます。DB検索や重い生成処理を置くならorElseGetを使います。
User user = cachedUser
.orElseGet(() -> repository.findDefaultUser());単純な定数ならorElseで十分です。どちらも『値なしを既定値へ置き換える』設計なので、未存在を隠してよい場面か確認します。
Optionalをフィールドや引数へ広げすぎない
Optionalは主に戻り値で値なしを表す用途に向きます。Entityフィールドやメソッド引数へ大量に使うと、シリアライズやフレームワークとの相性、呼び出しコードの複雑化が起きます。
プロジェクト規約に従い、引数の必須・任意はオーバーロードやForm、コマンドクラスで表すことも検討します。Optionalを使えばnull問題がすべて消えるわけではありません。
JUnitで存在・未存在を分ける
RepositoryをモックするServiceテストでは、Optional.ofだけでなくOptional.emptyを返すケースを必ず用意します。例外型と識別子も確認します。
@Test
void 利用者が存在しない場合は業務例外() {
when(repository.findById("U999"))
.thenReturn(Optional.empty());
UserNotFoundException exception = assertThrows(
UserNotFoundException.class,
() -> service.getUser("U999"));
assertEquals("U999", exception.getUserId());
}現場レビューでよくある指摘
Optionalのレビューではgetを禁止するだけでなく、値なしが正常か異常か、どの層で何へ変換するかを確認します。
// レビューコメント例
findByIdはOptional.emptyを返すため、getではNoSuchElementExceptionになります。
未存在時の業務例外をorElseThrowで明示してください。
// レビューコメント例
isPresentの直後にgetしています。存在時だけの処理ならifPresent、
必須ならorElseThrowの方が意図を表せます。
// レビューコメント例
orElseの引数でDB検索しているため、値があっても実行されます。
遅延実行が必要ならorElseGetを使ってください。Optionalチェーンを短くすることより、0件時の利用者体験と業務状態を説明できることを優先します。存在・未存在の両方をテストし、低レベル例外をそのまま500にしません。
提出前のセルフチェック
レビュー前に確認すること
- Optional.getを無条件に呼んでいないか
- 0件が正常か異常か決めたか
- 必須ならorElseThrowで例外を明示したか
- 存在時だけならifPresentが合うか
- mapで異なる未存在原因を潰していないか
- orElseで重い処理を実行していないか
- Optionalをフィールドや引数へ広げすぎていないか
- Optional.emptyのテストがあるか
Optionalと例外処理を実践的に学ぶ参考書
Optionalはラムダ、例外、Repositoryの戻り値とつながります。実践編で周辺APIも含めて確認すると、単なるget禁止ルールから一歩進んだ判断ができます。
スッキリわかるJava入門 実践編 第5版
基礎文法の次に必要な、現場寄りのJava知識を補う。
コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。
- Java基礎の次に何を学ぶか迷っている
- コレクションやStreamを整理したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Optional.getは値がなければNoSuchElementExceptionになります。必須値はorElseThrow、既定値はorElse・orElseGet、変換はmap、存在時だけの処理はifPresentを検討します。
ただしAPI選択より先に、0件を正常、404、業務エラーのどれにするか決めます。Optional.emptyのテストを追加し、低レベル例外を利用者へそのまま見せないでください。
