「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"));
Java0件以上 → 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これは「null を Optional に変えただけ」で、設計としては何も良くなっていません。
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 は“戻り値で“ないかもしれない”ことを型で伝えるための箱”として使い、フィールドや引数には安易に持ち込まない。」
