Java | Java 詳細・モダン文法:Optional – null 排除設計

Java Java
スポンサーリンク

「null 排除設計」を一言でいうと

「null 排除設計」は、
“アプリケーションの中を null が自由に歩き回らないようにする設計”
です。

完全に null をゼロにするのは現実的ではありませんが、
null が入り込む場所」と「null を扱う場所」を意識的に限定することで、
NullPointerException のリスクと「よく分からない null チェック地獄」を大きく減らせます。

その中心にいる道具が Optional です。


まず決めるべき大原則:「外向き API で null を返さない」

悪い例:戻り値で平然と null を返す

典型的な「null まみれ設計」はこんな感じです。

User findById(int id) {
    // 見つからなければ null を返す
}
Java

呼び出し側は、毎回こう書かないといけません。

User user = findById(10);
if (user != null) {
    System.out.println(user.getName());
}
Java

そして、どこかでこの if (user != null) を書き忘れた瞬間に NPE です。
しかも、「このメソッドは null を返す可能性があるのか?」は、コメントか実装を読まないと分かりません。

良い例:戻り値で「null の可能性」を型に閉じ込める

ここで「null 排除設計」の一歩目として、
「外向きのメソッドは null を返さない」というルールを置きます。

その代わりに、こうします。

Optional<User> findById(int id) {
    // 見つからなければ Optional.empty() を返す
}
Java

呼び出し側は、こう書くことになります。

findById(10)
        .ifPresent(user -> System.out.println(user.getName()));
Java

あるいは、デフォルト値を決めるならこうです。

String name =
        findById(10)
                .map(User::getName)
                .orElse("ゲスト");
Java

ここで重要なのは、
「このメソッドは“見つからない可能性がある”」という情報が、
Optional<User> という型に埋め込まれていることです。

呼び出し側は、null チェックを「忘れる」ことができません。
コンパイル時点で、「Optional をどう扱うか」を必ず決めさせられます。

これが null 排除設計のコアアイデアです。


フィールドと引数では「安易に Optional を使わない」

フィールドに Optional を持たせるとどうなるか

よくある誤解がこれです。

class User {
    Optional<Address> address; // こうしたくなる気持ちは分かるけど…
}
Java

一見「null 排除っぽく」見えますが、実際には扱いづらくなります。

フレームワークやシリアライズとの相性が悪くなる
Optional<Optional<T>> のような二重構造が生まれやすい
結局どこかで orElse(null) してしまいがち

など、かえって null が見えにくくなることが多いです。

null 排除設計の現実的な線引き

現実的な線引きはこうです。

クラスのフィールドやメソッド引数の世界(内部)は、
「null を許すかどうか」を自分たちのルールで決める。

外向きの API(サービスメソッドやリポジトリの戻り値)では、
「ないかもしれない 1 件」は Optional<T> で返し、
「0 件以上」は List<T>Stream<T> で返す。

つまり、
“外に向かう境界で null を Optional に閉じ込める”
というイメージです。


「null を返さない」ための具体的なパターン

1件 or なし → Optional

検索系メソッドで一番よく出てきます。

Optional<User> findByEmail(String email);
Java

呼び出し側は、必ず「なかったときどうするか」を決めます。

User user =
        findByEmail("a@example.com")
                .orElseThrow(() -> new IllegalArgumentException("not found"));
Java

0件以上 → List / Stream

「複数かもしれない」ものに null を返すのはやめます。

悪い例:

List<User> findByAge(int age); // 見つからなければ null を返すかも…
Java

良い例:

List<User> findByAge(int age); // 見つからなければ空リストを返す
Java

呼び出し側は、こう書けます。

List<User> users = findByAge(20);
System.out.println("件数: " + users.size());
Java

「空リスト」と「null」は意味が違います。
空リストは「結果はあるが、要素が 0 件」という“正常な状態”です。
null は「そもそも結果がない/返せなかった」という“異常寄りの状態”です。

null 排除設計では、「正常な“0 件”は空コレクションで表現する」を徹底します。


Optional を使うときの「やってはいけない」書き方

get() にすぐ飛びつかない

Optional を見て、ついこう書きたくなります。

Optional<User> maybeUser = findById(10);
User user = maybeUser.get(); // 中身がなければ NoSuchElementException
Java

これは「nullOptional に変えただけ」で、設計としては何も良くなっていません。

get() は「ここで空だったらバグ」という場面でだけ使うべきです。
それ以外は、orElse / orElseThrow / ifPresent / map / flatMap で「なかったときの扱い」を明示的に書きます。

Optional を引数にしない

これもやりがちです。

void sendMail(Optional<User> user) { ... }
Java

呼び出し側がこうなります。

sendMail(findById(10)); // Optional をそのまま渡す
Java

一見きれいですが、「メールを送るかどうか」という判断が sendMail の中に隠れてしまいます。

設計としては、こう分けた方がスッキリします。

findById(10)
        .ifPresent(this::sendMail); // sendMail(User user)
Java

「ユーザーがいるなら送る」という判断は呼び出し側で行い、
sendMail 自体は「必ずユーザーがいる前提」のメソッドにしておく。

これも「null を閉じ込める場所をはっきりさせる」ための工夫です。


「null 排除設計」をプロジェクトのルールに落とす

最低限決めておきたいルール例

チームでやるなら、最低限このくらいは明文化しておくと安定します。

戻り値で null を返さない。
1件 or なし → Optional<T>
0件以上 → 空コレクション(List<T> / Set<T> / Stream<T>
Optional をフィールドや引数に使わない。
Optional#get() は原則禁止(どうしても使うならコメントで理由を書く)。

これだけでも、「どこから null が飛んでくるか分からない」状態からはかなり脱出できます。


まとめ:null 排除設計を自分の言葉で言うなら

あなたの言葉でまとめると、こうなります。

「null 排除設計は、null を世界から消すのではなく、null が入り込む場所と扱う場所を意識的に限定する設計。
外向きの API では null を返さず、1件 or なしは Optional、0件以上は空コレクションで表現する。
Optional は“戻り値で“ないかもしれない”ことを型で伝えるための箱”として使い、フィールドや引数には安易に持ち込まない。」

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