Java | Java 詳細・モダン文法:Optional – Optional の誤用例

Java Java
スポンサーリンク

なぜ「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

isPresentget を見かけたら、“これ 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 で扱いを決める。

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