Java 逆引き集 | null/Optional の使い分け - NPE 回避、意図の明示

Java Java
スポンサーリンク

nullって何者か(「値がない」を表す原始的な記号)

Javaの参照型変数は、「どのオブジェクトも指していない状態」を null で表します。 「まだ設定されていない」「検索結果が見つからなかった」「外部から値が返ってこなかった」など、現実世界でよくある“値が存在しない”状況を、とりあえず全部 null に押し込めることができます。

String name = null;   // 名前がまだ決まっていない
User user = null;     // ユーザーが見つからなかった
Java

ただし null のままメソッドを呼んだりフィールドにアクセスすると、NullPointerException(NPE)が発生します。

String name = null;
System.out.println(name.length()); // ここで NPE
Java

「参照型変数に null を許す」という設計は、その瞬間から NPE のリスクを抱えることになります。 だからこそ、「どこで null を使うか」「どこで null を返さないと決めるか」が、設計の話になるわけです。

NPEが起きる仕組みと、nullチェックの基本パターン

参照型変数と「呼び先がない」状態

参照型変数は「オブジェクトへの矢印」です。 name.length() は「name が指しているオブジェクトに対して length を呼ぶ」という意味になります。

namenull のときは、「どこも指していない矢印」に対してメソッドを呼ぼうとしているので、Javaは「呼び先がない」と判断して NullPointerException を投げます。

NPEを避けるための一番素朴なパターンは、「使う前に null かどうかをチェックする」です。

String name = null;

if (name != null) {
    System.out.println(name.length());
} else {
    System.out.println("name はまだ設定されていません");
}
Java

この「nullチェック」は、Javaを書いていると何度も出てきます。 ただし、どこでもかしこでも null を許してしまうと、コードのあちこちが nullチェックだらけになり、「そもそもこの null は正常なのか、異常なのか」が分からなくなっていきます。

ビジネスロジックにおける null の意味を言語化する

重要なのは、「この null は何を意味しているのか」を自分で説明できるようにしておくことです。

検索結果が見つからなかったから null まだ設定されていないから null 外部APIが値を返さなかったから null

この「意味」が曖昧なまま null を使うと、「この null はエラーなのか、正常な“値なし”なのか」が読み手に伝わりません。 null を使うなら、「このメソッドは見つからないとき null を返す」「このフィールドは未設定を null で表す」といったルールを、コードとコメントで明示しておくことが大事です。

Optionalって何者か(「あるかもしれない」を型で表す箱)

Optionalの基本イメージ

Optional<T> は、「T 型の値があるかもしれないし、ないかもしれない」という状態を表すためのラッパーです。 Java 8 以降で導入されて、「null をそのまま返すより、意図を型で表現しよう」という発想から生まれています。

import java.util.Optional;

Optional<String> nameOpt = Optional.of("Taro");   // 値あり
Optional<String> emptyOpt = Optional.empty();     // 値なし
Java

null はただの「何もない」ですが、Optional<T> は「値がある/ない」という意図を型レベルで表現できます。 つまり、「このメソッドは結果がない可能性がありますよ」と、戻り値の型そのものが教えてくれるわけです。

Optionalを戻り値に使う例

「ユーザーが見つかるかもしれないし、見つからないかもしれない」メソッドを考えてみます。

import java.util.Optional;

public Optional<User> findUser(int id) {
    if (id == 1) {
        return Optional.of(new User(1, "Admin"));
    } else {
        return Optional.empty();
    }
}
Java

呼び出し側は、「結果があるかどうか」を必ず意識することになります。

Optional<User> userOpt = findUser(2);

// 値があるときだけ処理する
userOpt.ifPresent(user -> System.out.println(user.getName()));

// なければ代わりの値を使う
User user = userOpt.orElseGet(() -> new User(0, "Guest"));
Java

null を返す設計だと、「ヌルチェックを忘れる」という事故が起きやすいですが、 Optional を戻り値にすると、「このメソッドは結果がない可能性があります」と型が主張してくれるので、NPE回避につながります。

null と Optional の使い分けの軸

「内部実装」と「外部API」を分けて考える

まず大事なのは、「クラスの内部」と「外部に公開するAPI」を分けて考えることです。

クラス内部のフィールドやローカル変数では、null を使うこと自体はよくあります。 例えば「まだロードしていないキャッシュ」を null で持っておく、など。

public class UserCache {
    private User cachedUser; // 最初は null

    public User getOrLoad() {
        if (cachedUser == null) {
            cachedUser = loadFromDb();
        }
        return cachedUser;
    }
}
Java

ここでは、内部で null を使っていても、外に出すときは「必ず User を返す」という契約にしています。 つまり、「内部では null を使ってもいいが、APIとしては null を返さない」と決めることで、呼び出し側のNPEリスクを減らせます。

一方、「結果がないこと自体が仕様の一部」であるメソッド(検索、オプション設定など)では、戻り値に Optional<T> を使うと意図が明確になります。

public Optional<User> findUser(int id) { ... }  // 見つからない可能性があることを型で表現
Java

「内部では null を使ってもいい」「外部APIでは Optional で意図を明示する」という分け方は、現実的でバランスがいいです。

「絶対にある値」と「ないかもしれない値」を分ける

もう一つの軸は、「この値は絶対にあるべきか? それとも、ないことが普通にありうるか?」です。

絶対にあるべき値(ユーザーID、必須項目など)は、戻り値やフィールドで null を許さない設計にした方が安全です。 「null が来たらそれはバグ」という世界にしてしまう。

一方、「ないことが普通にありうる値」(任意のニックネーム、検索結果、オプション設定など)は、 Optional<T> で「ないことも仕様です」と型で表現すると、呼び出し側が必ずその可能性を考えるようになります。

public Optional<String> findNickname(int userId) { ... }
Java

ここで String をそのまま返して null を使うと、「ニックネームがないのか、バグで null になったのか」が曖昧になります。 Optional<String> にしておけば、「ニックネームがないのは普通のケース」と読み手に伝わります。

Optional を使うときの注意点(やりすぎ防止)

フィールドや引数には基本使わない

Optional は「戻り値」に使うのが基本で、フィールドやメソッド引数にはあまり使いません。

// あまり推奨されない例
public class User {
    private Optional<String> nickname; // フィールドに Optional
}
Java

フィールドに Optional を持つと、「中身があるかどうかをさらに Optional で包む」という二重構造になり、 結局内部で null を使うのとあまり変わらない複雑さを生みます。

引数に Optional を使うのも、「呼び出し側が Optional を作って渡さなきゃいけない」という余計な負担になります。

// これもあまり推奨されない
public void setNickname(Optional<String> nickname) { ... }
Java

「値があるかどうかを意識してほしい」のは、主に戻り値側なので、 基本は「戻り値にだけ Optional を使う」と覚えておくとシンプルです。

コレクションの中身に Optional を入れない

List<Optional<T>> のような構造も、初心者のうちは避けた方がいいです。

List<Optional<User>> users; // これはだいぶ読みにくくなる
Java

「ユーザーがいるかいないか」を表現したいなら、 「見つかったユーザーだけをリストに入れる」「見つからなかったものはそもそもリストに入れない」といった設計の方が自然です。

Optional は「1つの値があるかないか」を表現するための道具であって、 「集合の中であるかないか」を表現するには、別の設計(フィルタリング、別リストなど)の方が向いています。

まとめと小さな練習

null は「値がない」を表す原始的な記号で、強力だけど NPE のリスクを常に抱えます。 Optional は「値があるかもしれないし、ないかもしれない」という意図を型で表現する箱で、 戻り値に使うことで「このメソッドは結果がない可能性があります」と呼び出し側に強く伝えられます。

練習として、次のようなことを自分の手で試してみると感覚がつかめます。

練習1:null版とOptional版の検索メソッドを書き比べる

User findUserOrNull(int id)Optional<User> findUser(int id) を両方書いて、 呼び出し側のコードがどう変わるか、ヌルチェックの書き方がどう変わるかを比べてみてください。

練習2:任意項目をOptionalで表現する

ユーザーの「ニックネーム」や「紹介文」のような任意項目を、 Optional<String> getNickname() のような戻り値で表現してみて、 呼び出し側で orElseifPresent を使って処理するコードを書いてみてください。

「この値は絶対にある」「この値はないことも普通にある」を自分で線引きして、 null と Optional を意図的に使い分けられるようになると、NPEに怯えない設計ができるようになっていきます。

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