テックブログ

Java Optional入門:nullチェックを減らす使い方と注意点

Java Optional入門:nullチェックを減らす使い方と注意点

JavaのOptionalは、値が存在する場合と存在しない場合を型として表すための仕組みです。戻り値にOptionalを使うことで、呼び出し側に「値がない可能性がある」と明示でき、雑なnullチェックやNullPointerExceptionにつながる実装を減らしやすくなります。ただし、Optionalを使えばすべてのnull問題が消えるわけではなく、get()の乱用や、フィールド・引数へ何でも使う設計には注意が必要です。

1. nullとNullPointerExceptionとは

1-1. nullは参照先がない状態

nullは、参照型の変数がどのオブジェクトも指していない状態です。Javaでは、クラス型や配列、インターフェース型などの参照型変数にnullが入ることがあります。

たとえば、String nameという変数は、文字列オブジェクトを参照できます。しかし、値が未設定だったり、検索結果が見つからなかったりすると、nullが入ることがあります。この状態でメソッドを呼ぼうとすると、参照先がないため処理できません。

String name = null;

// nameは文字列オブジェクトを参照していない
System.out.println(name);

このコードでは、namenullであり、文字列オブジェクトを指していません。注意点は、null自体が悪いというより、「値がない可能性」がコード上で分かりにくいまま扱われることです。

1-2. NullPointerExceptionが起きる典型例

NullPointerExceptionは、nullに対してメソッド呼び出しやフィールド参照を行ったときに起きる代表的な実行時エラーです。Java初心者が実務でよく遭遇するエラーのひとつです。

変数にオブジェクトが入っている前提で処理を書いていると、実際にはnullだった場合に失敗します。特に、DB検索結果、外部APIレスポンス、入力値、設定値などは、値が存在しない可能性があります。その可能性を考えずにメソッドを呼ぶと、NullPointerExceptionにつながります。

String userName = findUserNameById("U001");

System.out.println(userName.length());

このコードでは、findUserNameByIdnullを返す可能性がある場合、userName.length()NullPointerExceptionが起きます。注意点は、メソッド名だけでは「見つからないときにnullが返るのか」が分かりにくいことです。

1-3. nullチェックが増えると読みにくくなる理由

nullチェックが増えすぎると、本来読みたい業務処理の流れが見えにくくなります。安全にするつもりのチェックが、コード全体の見通しを悪くすることがあります。

値がない可能性がある場所が多いと、if (value != null)のような条件分岐があちこちに増えます。その結果、「何をしたい処理なのか」よりも「nullかどうかを確認する処理」が目立つようになります。また、どこでnullを許可しているのか、どこからは必ず値があるのかも曖昧になりがちです。

if (user != null) {
    if (user.getAddress() != null) {
        if (user.getAddress().getCity() != null) {
            System.out.println(user.getAddress().getCity());
        }
    }
}

このコードでは、nullを避けるための条件分岐が深くなっています。注意点は、Optionalはこのようなコードを必ず短くする魔法ではないことです。まずは「値がない可能性をどこで表現するか」を設計することが大切です。

2. Optionalとは何か

2-1. 値がある/ない可能性を型で表す

Optionalは、値がある場合とない場合を型として表すためのクラスです。戻り値に使うことで、呼び出し側に「この結果は存在しない可能性があります」と明示できます。

従来のnull返却では、メソッドの利用者がドキュメントや実装を見ないと、nullが返る可能性に気づきにくいことがあります。一方、戻り値がOptional<User>なら、呼び出し側は値がない場合を考慮する必要があると分かります。つまり、Optionalは例外処理ではなく、値がない可能性の表現に使う道具です。

Optional<User> user = findUserById("U001");

このコードでは、ユーザーが見つかる場合も見つからない場合もあることが型から分かります。注意点は、Optionalを使っても中身の扱いを誤ればエラーは起きることです。Optionalは設計意図を明確にする道具として使いましょう。

2-2. Optional.empty() と Optional.ofNullable()

Optional.empty()は値がない状態を表し、Optional.ofNullable()nullかもしれない値をOptionalに包むために使います。Optionalの基本を理解するうえで重要なメソッドです。

Optional.empty()は、検索結果がない、設定値が存在しないなど、明示的に値がないことを返したい場合に使います。Optional.ofNullable(value)は、valuenullなら空のOptional、nullでなければ値を持つOptionalを作ります。一方、Optional.of(value)valuenullだと例外になるため、使い分けが必要です。

Optional<String> emptyName = Optional.empty();

String rawName = getNameOrNull();
Optional<String> name = Optional.ofNullable(rawName);

このコードでは、値がない状態と、nullの可能性がある値をOptionalへ変換する例を示しています。注意点は、Optional.of(null)を書かないことです。nullの可能性があるならofNullableを使いましょう。

2-3. 戻り値で使うのが基本

Optionalは、値がない可能性のある戻り値で使うのが基本です。特に、検索系メソッドや取得できない可能性があるメソッドで効果を発揮します。

戻り値にOptionalを使うと、呼び出し側は値がないケースを自然に意識できます。たとえば、ユーザーIDで検索して見つからない可能性があるなら、Usernullを返すより、Optional<User>を返すほうが意図が伝わりやすくなります。

public Optional<User> findById(String userId) {
    User user = userRepository.find(userId);
    return Optional.ofNullable(user);
}

このコードでは、見つからない可能性のある検索結果をOptionalで返しています。注意点は、フィールド、メソッド引数、コレクションの要素まで何でもOptionalにする必要はないことです。Optionalは主に戻り値で「ない可能性」を伝えるために使うと整理しやすいです。

3. Optionalの基本的な使い方

3-1. isPresent() / isEmpty()

isPresent()isEmpty()は、Optionalに値が入っているかを確認するためのメソッドです。単純に存在有無で分岐したい場合に使えます。

isPresent()は値があるときにtrueを返し、isEmpty()は値がないときにtrueを返します。Java 11以降ではisEmpty()が使えるため、「空なら処理する」という条件が読みやすくなります。ただし、isPresent()で確認した直後にget()する書き方ばかりになると、従来のnullチェックとあまり変わらない場合があります。

Optional<User> user = findById("U001");

if (user.isPresent()) {
    System.out.println(user.get().getName());
}

if (user.isEmpty()) {
    System.out.println("ユーザーが見つかりません");
}

このコードでは、Optionalの有無で処理を分けています。注意点は、get()を使う場合は必ず値の存在確認が必要なことです。より読みやすくできる場面では、orElsemapなども検討しましょう。

3-2. orElse() / orElseGet()

orElse()orElseGet()は、Optionalが空だった場合の代替値を返すためのメソッドです。値がないときのデフォルト値を用意したい場面で使います。

orElse()は、Optionalに値があっても引数の式が先に評価されます。一方、orElseGet()は、Optionalが空の場合だけSupplierが実行されます。代替値の生成が軽い場合はorElse()で問題になりにくいですが、DBアクセスや重い計算、ログ出力などを含む場合はorElseGet()のほうが意図に合うことがあります。

String name1 = findName("U001").orElse("名無し");

String name2 = findName("U001").orElseGet(() -> createDefaultName());

このコードでは、値がない場合に代替の名前を返しています。注意点は、orElse(createDefaultName())と書くと、Optionalに値がある場合でもcreateDefaultName()が実行されることです。重い処理や副作用がある処理ではorElseGet()を検討しましょう。

3-3. map() / filter() の入口

map()filter()を使うと、Optionalの中に値がある場合だけ変換や条件判定を行えます。nullチェックの分岐を減らし、処理の流れを読みやすくできる場合があります。

map()は、中身が存在する場合にだけ関数を適用して別の値へ変換します。filter()は、中身が存在し、条件に合う場合だけ値を残します。中身が空なら処理は実行されず、空のOptionalのまま進みます。これにより、「値があれば処理する、なければ何もしない」という流れを表現できます。

Optional<String> email = findById("U001")
    .map(User::getEmail)
    .filter(value -> value.endsWith("@example.com"));

このコードでは、ユーザーが見つかった場合だけメールアドレスを取り出し、特定ドメインの場合だけ残しています。注意点は、チェーンを長くしすぎると逆に読みにくくなることです。複雑な業務条件は、無理にOptionalのチェーンへ押し込まず、通常の条件分岐に分けても問題ありません。

4. Optionalでやりがちなミス

4-1. get()を雑に使う

Optionalで最も避けたいミスは、値があるか確認せずにget()を呼ぶことです。これはnullに対してメソッドを呼ぶ問題と似た危険があります。

Optional.get()は、中に値がある場合はその値を返しますが、空の場合はNoSuchElementExceptionを投げます。つまり、get()を雑に使うと、NullPointerExceptionを別の例外に置き換えただけになってしまいます。Optionalの目的は、値がない可能性を安全に扱うことであり、無条件に中身を取り出すことではありません。

// 悪い例
Optional<User> user = findById("U001");
System.out.println(user.get().getName());

// 良い例
String name = findById("U001")
    .map(User::getName)
    .orElse("未登録ユーザー");

System.out.println(name);

このコードでは、悪い例は空の可能性を無視してget()を呼んでいます。良い例では、ユーザーが存在する場合だけ名前を取り出し、存在しない場合は代替値を使います。get()は基本形ではなく、どうしても値があると保証できる場面に限って慎重に使いましょう。

4-2. フィールドや引数に何でもOptionalを使う

Optionalは便利ですが、フィールドやメソッド引数に何でも使うことは推奨されません。使いすぎると、かえって設計やコードが読みにくくなることがあります。

Optionalは主に「戻り値で値がない可能性を明示する」ために設計された使い方が多く、フィールドや引数に使うと扱いが複雑になる場合があります。たとえば、DTOのすべての項目をOptional<String>にすると、JSON変換やフレームワーク連携、入力バリデーションで扱いにくくなることがあります。引数でも、呼び出し側にOptional.ofNullableを書かせるだけなら、意味が薄いことがあります。

// 避けたい例になりやすい
public void updateName(Optional<String> name) {
    // 呼び出し側も扱いが面倒になる
}

// シンプルに設計する例
public void updateName(String name) {
    // nullを許可するかどうかはメソッド仕様やバリデーションで決める
}

このコードでは、引数にOptionalを使うことで呼び出し側の負担が増えやすい例を示しています。注意点は、Optionalを使わないことが常に正解という意味ではないことです。戻り値で「存在しない可能性」を表す場面に絞ると、使いどころが分かりやすくなります。

4-3. orElse() と orElseGet() の違いを知らない

orElse()orElseGet()の違いを知らないと、不要な処理が実行されたり、思わぬ副作用が起きたりすることがあります。特に代替値を作る処理が重い場合に注意が必要です。

orElse()の引数は、Optionalに値がある場合でも評価されます。つまり、代替値としてメソッド呼び出しを書くと、そのメソッドは毎回実行されます。一方、orElseGet()はOptionalが空のときだけ実行されます。この違いは、ログ出力、DBアクセス、API呼び出し、ランダム値生成などを含む場合に重要です。

// 値があっても createDefaultUser() は実行される
User user1 = findById("U001").orElse(createDefaultUser());

// 値がない場合だけ createDefaultUser() が実行される
User user2 = findById("U001").orElseGet(() -> createDefaultUser());

このコードでは、orElseorElseGetの評価タイミングが違います。注意点は、単純な文字列や定数ならorElseで十分なことも多いことです。代替値の生成に処理コストや副作用がある場合はorElseGetを選びましょう。

5. 実務での使いどころ

5-1. 検索結果がない可能性を戻り値で表す

Optionalが特に役立つのは、検索結果がない可能性を戻り値で表したい場面です。DB検索、キャッシュ検索、設定値取得などでよく使えます。

たとえば、IDでユーザーを検索しても、対象のユーザーが存在しないことがあります。このときnullを返すと、呼び出し側が見落とす可能性があります。Optional<User>を返せば、利用者は「存在しない場合」をコード上で意識しやすくなります。

public Optional<User> findUserByEmail(String email) {
    User user = userRepository.findByEmail(email);
    return Optional.ofNullable(user);
}

このコードでは、メールアドレスで見つからない可能性をOptionalで表しています。注意点は、「存在しない」が正常な結果なのか、異常な状態なのかを分けて考えることです。存在しないことが業務エラーなら、Optionalではなく例外やエラーレスポンスで扱う設計もあります。

5-2. nullを返すメソッドを減らす

Optionalを使うと、新しく作るメソッドでnullを返す設計を減らしやすくなります。戻り値の意味が明確になり、呼び出し側の見落としを防ぎやすくなります。

既存コードでは、見つからない場合にnullを返すメソッドが多く存在することがあります。そのようなメソッドをすべて一度に直すのは難しいですが、新しく作る検索系メソッドではOptionalを返す方針にすると、コードの意図を統一しやすくなります。境界部分でOptional.ofNullableを使えば、既存のnull返却をOptionalへ変換できます。

// 既存メソッド
User rawUser = legacyFindUser("U001");

// Optionalに包んで扱う
Optional<User> user = Optional.ofNullable(rawUser);

このコードでは、既存のnullを返す可能性があるメソッドの結果をOptionalに変換しています。注意点は、内部では結局nullが存在しうることです。Optionalに包んだあとは、get()で雑に取り出さず、空の場合の処理を明確にしましょう。

5-3. ただし無理に全部Optional化しない

Optionalは便利ですが、すべてのnull可能性をOptionalに置き換えればよいわけではありません。無理に使うと、コードがかえって複雑になることがあります。

たとえば、必須項目であるはずの値にはOptionalを使うより、生成時に必ず値を渡す、バリデーションで弾く、コンストラクタで不正状態を作れないようにする、といった設計が向いています。また、リストが空であることを表したいなら、Optional<List<T>>より空のListを返すほうが自然な場合も多いです。

判断の目安は、「値がないことが通常の結果としてあり得るか」です。検索して見つからないならOptionalが向いています。一方、必須のユーザー名や注文金額がないなら、Optionalで包むより入力エラーや設計上の不正として扱うべき場合があります。

6. まとめ

6-1. Optionalは値がない可能性を伝える道具

Optionalは、値がない可能性を戻り値として明示するための道具です。NullPointerExceptionを完全になくす魔法ではなく、呼び出し側に「ない場合を考えてください」と伝える型です。

Optional.empty()は値がない状態、Optional.ofNullable()nullかもしれない値の変換に使えます。値を取り出すときは、get()を雑に使うのではなく、orElseorElseGetmapfilterなどを使って、値がない場合の動きを明確にします。

特に実務では、検索結果がない可能性を表す戻り値にOptionalを使うと、コードの意図が伝わりやすくなります。ただし、フィールドや引数、コレクション要素まで何でもOptionalにするのではなく、使いどころを絞ることが大切です。

6-2. 新人向けチェックリスト

Optionalを使うときは、値がない可能性の表現、取り出し方、使いすぎを確認すると失敗を減らせます。

  • 値がない可能性を戻り値で表したい場面か
  • nullを返す代わりにOptional.empty()を使えるか
  • nullかもしれない値にはOptional.ofNullable()を使っているか
  • Optional.of(null)を書いていないか
  • get()を存在確認なしで呼んでいないか
  • 代替値が重い処理ならorElseGet()を検討しているか
  • フィールドや引数に何でもOptionalを使っていないか
  • 空のリストで表せる場面をOptional<List<T>>にしていないか
  • 「存在しない」が正常結果なのか、エラーなのかを分けて考えているか

Optionalを正しく使うと、nullチェックをただ増やすのではなく、値がない可能性を読みやすく表現できます。重要なのは、Optionalを使うこと自体ではなく、値がない場合の扱いをコード上で明確にすることです。

7. 参考リンク