なぜ「Optional の誤用」を意識する必要があるのか
Optional は「null を安全に扱うための道具」ですが、使い方を間違えると、むしろコードが読みにくくなったり、バグの温床になったりします。
「Optional を使っているからモダンで安全」ではなく、「Optional を“設計として”正しく使えているか」が本質です。
ここでは、現場で本当によく見かける誤用パターンを取り上げて、「なぜそれがまずいのか」「どう書き直すといいのか」をかみ砕いて見ていきます。
誤用1:フィールドに Optional を持たせる
ありがちなコード例
まず、かなりよく見るパターンがこれです。
class User {
private Optional<Address> address; // 住所がないかもしれないから Optional にしておこう…
}
Java一見「null 排除していて良さそう」に見えますが、これは Optional の設計思想から外れています。
なぜまずいのか
Optional は「戻り値専用のコンテナ」として設計されています。
フィールドに使い始めると、次のような問題が出てきます。
フレームワーク(JPA, Jackson など)との相性が悪くなる。Optional<Optional<T>> のような二重構造が生まれやすくなる。
結局どこかで orElse(null) してしまい、null が復活する。
「内部表現として Optional を持つ」よりも、「外向きの API で Optional を返す」方がずっと筋が良いです。
どう直すとよいか
フィールドは素直にこうします。
class User {
private Address address; // null 許容かどうかはクラス内のルールで決める
}
Javaそして、外向きのメソッドで Optional に包みます。
Optional<Address> getAddressOptional() {
return Optional.ofNullable(address);
}
Javaこうすると、「クラスの外からは Optional で安全に扱える」「中ではフレームワークとも素直に連携できる」というバランスが取れます。
誤用2:メソッド引数に Optional を使う
ありがちなコード例
これもよく見ます。
void sendMail(Optional<User> user) {
if (user.isPresent()) {
// 送る
}
}
Java呼び出し側はこうなります。
sendMail(findUser()); // Optional<User> をそのまま渡す
Java一見「Optional をちゃんと使っている」ように見えますが、設計としては微妙です。
なぜまずいのか
「このメソッドは Optional を受け取る」ということは、
「このメソッドの中で、“いるかいないか”の判断をする」
という責務を押し込んでいることになります。
呼び出し側から見ると、
「ユーザーがいないときに何が起きるのか?」
「送らないだけなのか、例外なのか、ログなのか?」
がコードから読み取りにくくなります。
どう直すとよいか
責務を分けます。
void sendMail(User user) {
// ここは「必ずユーザーがいる前提」の処理だけを書く
}
Javaそして呼び出し側で Optional を処理します。
findUser()
.ifPresent(this::sendMail);
Java「いるかいないかの判断」は呼び出し側の責任。
「いたときに何をするか」はメソッドの責任。
この分離ができていると、Optional の設計が一気にきれいになります。
誤用3:Optional に null を入れる/ofNullable の乱用
ありがちなコード例
Optional<String> name = Optional.of(null); // 即例外
Optional<String> name2 = Optional.ofNullable(null); // empty にはなるが…
Javaあるいは、何でもかんでもとりあえず ofNullable で包むパターンです。
Optional<String> name = Optional.ofNullable(getNameOrNull());
Javaなぜまずいのか
Optional.of(null) は単純にバグです。
Optional は「null を入れる箱」ではなく、「null の代わりに使う箱」です。
ofNullable 自体は便利ですが、「そもそもそのメソッドが null を返す設計でいいのか?」を考えずに乱用すると、
「null をばらまくメソッド」+「とりあえず ofNullable で包むメソッド」
という、責任のよく分からない構造になります。
どう直すとよいか
外向きの API では、最初から Optional を返すように設計します。
Optional<User> findById(long id); // null は返さない
Javaどうしても古いコードやライブラリが null を返すなら、
「境界」でだけ ofNullable を使い、そこから先は Optional ベースで扱う、という線引きをします。
Optional<User> findById(long id) {
return Optional.ofNullable(legacyFindById(id)); // ここで null を閉じ込める
}
Java誤用4:isPresent と get のコンボ多用(null チェックの焼き直し)
ありがちなコード例
Optional<User> maybeUser = findUser();
if (maybeUser.isPresent()) {
User user = maybeUser.get();
System.out.println(user.getName());
}
Javaあるいはもっとひどいと、
if (maybeUser.isPresent() && maybeUser.get().getAge() >= 18) {
doSomething(maybeUser.get());
}
Javaというように、get() が何度も出てきます。
なぜまずいのか
これは「null チェック+変数」の焼き直しにすぎません。
User user = findUserOrNull();
if (user != null) {
...
}
Javaと本質的に同じで、Optional のメリット(map / flatMap / filter で宣言的に書ける)を捨てています。
さらに、isPresent() と get() のセットをどこかで書き忘れた瞬間に、NoSuchElementException が飛びます。
どう直すとよいか
map / flatMap / filter / orElse / orElseThrow / ifPresent を組み合わせて、「unwrap せずに最後まで書く」ことを目指します。
findUser()
.map(User::getName)
.ifPresent(System.out::println);
Java条件付きなら、
findUser()
.filter(u -> u.getAge() >= 18)
.ifPresent(this::doSomething);
Java「isPresent と get を見かけたら、“これ map / filter / orElse で書けないか?”と疑う」
この癖をつけると、Optional の設計が一気に洗練されます。
誤用5:戻り値が Optional なのに、すぐ get して投げ直すだけ
ありがちなコード例
Optional<User> maybeUser = repository.findById(id);
if (maybeUser.isEmpty()) {
throw new UserNotFoundException(id);
}
User user = maybeUser.get();
Javaあるいはもっと短く、
User user = repository.findById(id).get(); // empty なら NoSuchElementException
Javaなぜまずいのか
せっかく Optional を返しているのに、「呼び出し側で即 unwrap して例外にする」だけだと、
「だったら最初から Optional じゃなくてよくない?」という状態になります。
特に get() 直書きは、「ここで empty にならない前提」がコードから読み取れず、将来のバグの温床になります。
どう直すとよいか
「どのレイヤーまで Optional のまま運び、どのレイヤーで orElseThrow して“必ずある前提”に切り替えるか」を決めます。
User loadUserOrFail(long id) {
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
Javaそして、上のレイヤーではもう Optional を意識せずに使います。
User user = loadUserOrFail(id);
process(user);
Java「Optional を返すメソッド」と「必ずある前提のメソッド」を分けることで、コードの意図がはっきりします。
誤用6:コレクションに Optional を入れる/Optional のコレクションを返す
ありがちなコード例
List<Optional<User>> users = List.of(
Optional.of(new User("Alice")),
Optional.empty(),
Optional.of(new User("Bob"))
);
Javaあるいは、
List<Optional<User>> findUsers(...);
Javaのように、「Optional のリスト」を返す設計です。
なぜまずいのか
「0 個以上」の世界(List, Stream)と、「0 or 1 個」の世界(Optional)が混ざってしまっています。
「いないユーザー」は、
リストの中に Optional.empty() として存在するのか
そもそもリストに含めないのか
がコードから読み取りにくくなります。
どう直すとよいか
基本方針はこうです。
「0 個以上」は List / Stream で表現し、「いないもの」はそもそも含めない。
「1 件かもしれない」は Optional で表現する。
例えば、ID のリストから「存在するユーザーだけ」を集めたいなら、
List<User> users =
ids.stream()
.map(this::findById) // Optional<User>
.flatMap(Optional::stream) // いるユーザーだけ流す
.toList();
Javaのように、「途中で Optional を使い、最終的には List に落とす」設計にします。
まとめ:Optional の誤用を避けるための感覚
最後に、Optional を使うときに頭の片隅に置いておくといい感覚をまとめます。
Optional は「戻り値で“ないかもしれない 1 件”を表現するための箱」。
フィールドや引数、コレクションの中に持ち込むと、だいたい設計がややこしくなる。
isPresent / get コンボは「null チェックの焼き直し」なので、見かけたら map / flatMap / filter / orElse / orElseThrow に置き換えられないかを考える。
「ここで値がないのは普通か? 異常か?」を毎回自分に問い、普通なら orElse 系、異常なら orElseThrow で扱いを決める。
