JavaのOptional返却でnull契約が曖昧な実装をどうレビューするか
JavaのOptional返却でnull契約が曖昧な実装をどうレビューするか
Javaの Optional は、値が存在しない可能性を戻り値で表現できる。
しかしレビューでは、Optional を使っているだけで安全と判断してはいけない。
危ないのは次のような実装だ。
Optional<User>を返すメソッドがnullを返す- 呼び出し側がすぐ
optional.get()している - 見つからない場合とシステムエラーが同じ
Optional.empty()になる List<T>でよい戻り値をOptional<List<T>>にしている- フィールドや引数まで
Optionalで埋めて契約が読めない
この記事ではOptionalの文法ではなく、
未存在をどう契約として表現するかをレビューで確認する観点を整理する。
まず止めたい実装
public Optional<User> findUser(String userId) {
User user = userRepository.findById(userId);
if (user == null) {
return null;
}
return Optional.of(user);
}
public String userName(String userId) {
return findUser(userId).get().name();
}@Reviewer: `Optional` を返す契約なのに `null` を返しているため、呼び出し側はOptionalとnullの両方を考慮する必要があります。未存在は `Optional.empty()` に統一してください。 このコードは、戻り値の型だけ見ると未存在を表現しているように見える。
しかし実際には、Optional 自体が null になり得るため契約が崩れている。
レビューでは、Optionalがあるかではなく、
呼び出し側から見た分岐契約が単純になっているかを見る必要がある。
なぜ危ないのか
Optionalの使い方を誤ると、null安全のつもりで分岐が増える。
起きやすい問題は次の通りである。
Optionalとnullの二重チェックが必要になるget()によって未存在時に例外が出る- DB接続失敗などの異常系が
emptyに潰れる - 空リストと未取得の意味が混ざる
- API利用者が「ない」の意味を読み取れない
Optionalは、値がないことを明示するための型である。
レビューでは、値がないことが正常系なのか、異常系なのかを先に確認したい。
レビューで見たい3つの判断線
1. Optional自体をnullにしていないか
Optional<T> を返すなら、未存在は Optional.empty() にする。
Optional 自体がnullになる設計は、Optionalを使う意味を壊す。
public Optional<User> findUser(String userId) {
return Optional.ofNullable(userRepository.findById(userId));
}@Reviewer: 戻り値が `Optional` である以上、メソッドは常にOptionalインスタンスを返す契約にしてください。未存在は `Optional.empty()`、異常は例外やエラー型で分けてください。2. get() 前提の呼び出しになっていないか
optional.get() は、存在確認済みであることがコードから読める場合に限りたい。
すぐ get() するなら、Optionalの契約が呼び出し側で活きていない。
@Reviewer: `findUser(...).get()` により未存在時の分岐が呼び出し側で表現されていません。`orElseThrow` で例外契約を明示するか、未存在時の代替処理をコードに出してください。たとえば必ず存在しなければならない業務なら、orElseThrow で例外の意味を明示する。
User user = findUser(userId)
.orElseThrow(() -> new UserNotFoundException(userId));3. 空リストで表せるものをOptionalにしていないか
複数件検索では、結果が0件でも正常であることが多い。
その場合は Optional<List<User>> より List<User> の空リストの方が自然である。
@Reviewer: 複数件検索の戻り値が `Optional>` になっていますが、0件は正常結果に見えます。未取得と0件を分ける要件がないなら、空リストを返す契約にしてください。
Optionalは「値が1つあるかないか」に向いている。
コレクションの空は、コレクション自体で表現できることが多い。
改善例
未存在、必須存在、複数件をそれぞれ別の契約にする。
public Optional<User> findUser(String userId) {
return userRepository.findById(userId);
}
public User requireUser(String userId) {
return findUser(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
public List<User> searchUsers(UserSearchCondition condition) {
return userRepository.search(condition);
}
public UserProfile getProfile(String userId) {
User user = requireUser(userId);
return profileMapper.toProfile(user);
}この構造なら、レビューアーは次を確認しやすい。
- 任意存在は
Optional<User>で表現されている - 必須存在は
requireUserで例外契約に変換されている - 複数件検索は空リストで0件を表現している
- Optional自体がnullになる余地がない
- 呼び出し側で未存在の扱いが読める
Optional.empty() に潰してはいけないもの
未存在とシステムエラーは違う。
DB接続失敗、権限不足、外部API障害などを Optional.empty() にすると、呼び出し側は「存在しなかった」と誤認する。
public Optional<User> findUser(String userId) {
try {
return userRepository.findById(userId);
} catch (SQLException e) {
return Optional.empty();
}
}@Reviewer: DBエラーを `Optional.empty()` に変換しており、未存在と障害が区別できません。存在しないことが正常な場合だけemptyにし、取得失敗は例外として文脈を残してください。Optionalは失敗を隠す型ではない。
「ない」と「取れなかった」を混ぜないことが重要である。
フィールドや引数のOptionalは慎重に見る
戻り値のOptionalは契約として読みやすい。
一方で、DTOのフィールドやメソッド引数にOptionalを多用すると、利用側の記述が重くなりやすい。
@Reviewer: DTOフィールドが `Optional` になっていますが、シリアライズやBean validationとの境界が分かりにくくなります。外部入出力ではnullable fieldとして扱い、ドメイン境界でOptional戻り値に変換する方が読みやすいか確認してください。 Optionalをどこで使うかは、チームの規約にも左右される。
レビューでは、少なくとも外部境界とドメイン境界が混ざっていないかを確認したい。
レビュー観点チェックリスト
Optional<T>を返すメソッドがnullを返していないか- 未存在は
Optional.empty()、異常は例外として分かれているか - 呼び出し側がすぐ
get()していないか - 必須存在なら
orElseThrowなどで例外契約が明示されているか - 複数件検索に
Optional<List<T>>を使っていないか - DTOフィールドや引数にOptionalを広げすぎていないか
- API利用者から「ない」の意味が読めるか
まとめ
JavaのOptionalレビューでは、Optionalを使っているかではなく、未存在の契約が単純になっているかを見る。
特に Optional自体のnull返却、get乱用、異常系のempty化 は早めに止めたい。
Optionalはnullを隠すためではなく、値がないことを呼び出し側へ明示するための型である。
レビューでは、その明示が実際に呼び出し側の判断を助けているかを確認する。