【Java】JavaのOptionalでnullを安全に扱う方法

Java

こんにちは、かつコーチです。
「メソッドの戻り値がnullかもしれないから、呼び出し側で毎回nullチェックを書く」というのは、地味に手間がかかる作業です。
今回はそんなnullチェックの負担を減らしてくれるOptionalの使い方を解説します。

Optionalとは?

値があるかもしれないことを表す入れ物

Optional(値が存在するかもしれないし、存在しないかもしれないことを表現する入れ物クラス)は、Optional<String>のように使い、中身が空である可能性をコード上で明示できます。
戻り値の型を見ただけで「このメソッドは値が無い場合があるので、呼び出し側で対応してね」と伝えられるのが最大のメリットです。

なぜOptionalが必要なのか

Optionalが無い場合、戻り値がnullになる可能性のあるメソッドは、シグネチャだけを見てもそれが分かりません。

// 戻り値だけ見ても、nullが返る可能性があるかどうか分からない
public static String findNameById(int id) {
    if (id == 1) {
        return "かつコーチ";
    }
    return null; // 見つからない場合はnull
}
String name = findNameById(2);
System.out.println(name.length()); // 実行時エラー(NullPointerException)

呼び出し側がnullチェックを忘れると、実行時にNullPointerExceptionが発生してしまいます。
Optionalを使うと、この「nullかもしれない」という情報を型として表現でき、呼び出し側にチェックを促せます。

基本の書き方

Optionalを返すメソッドを定義する

import java.util.Optional;

public static Optional<String> findNameById(int id) {
    if (id == 1) {
        return Optional.of("かつコーチ");
    }
    return Optional.empty(); // 値が無いことを明示する
}

Optional.of(値)で値ありのOptionalを、Optional.empty()で値なしのOptionalを作ります。
値がnullかどうか分からない場合は、Optional.ofNullable(値)を使うとnullを渡してもエラーにならずに空のOptionalへ変換されます。

isPresent・getで中身を確認する

Optional<String> result = findNameById(1);

if (result.isPresent()) {
    System.out.println(result.get()); // かつコーチ
} else {
    System.out.println("見つかりませんでした");
}

ただし、このisPresent() + get()の組み合わせは、実はOptionalの本来の使い方としては推奨されていません(詳しくは次のセクションで解説します)。

orElse・orElseGetでデフォルト値を用意する

値が無い場合のデフォルト値をその場で指定できるのが、Optionalらしい書き方です。

Optional<String> result = findNameById(2);

String name = result.orElse("名無しさん");
System.out.println(name); // 名無しさん

orElseGetを使うと、デフォルト値の生成処理を必要になったときだけ実行できます。

String name2 = result.orElseGet(() -> {
    System.out.println("デフォルト値を生成します");
    return "ゲスト";
});
System.out.println(name2); // ゲスト

固定値でよいならorElse、生成コストがかかる場合はorElseGetと使い分けます。

よくあるつまずきポイント・エラー対処

isPresent + getを多用してnullチェックと変わらないコードになった

私が実際にレビューでよく見かける一次情報として、Optionalを導入したのに結局isPresentgetばかり使い、従来のnullチェックと変わらない書き方になっているケースがあります。

// ❌Before:isPresent + getでnullチェック時代と同じ書き方になっている
Optional<String> result = findNameById(1);

if (result.isPresent()) {
    String name = result.get();
    System.out.println("こんにちは、" + name + "さん");
} else {
    System.out.println("ユーザーが見つかりません");
}

このコードは動きますが、Optionalが持つ「値の有無に応じた処理を関数的に書ける」という利点を活かせていません。
さらに、isPresent()を確認せずにget()を呼んでしまうと次の例外が発生します。

Exception in thread "main" java.util.NoSuchElementException: No value present
    at java.base/java.util.Optional.get(Optional.java:143)
// ✅After:ifPresentOrElseで値の有無に応じた処理を関数的に書く
Optional<String> result = findNameById(1);

result.ifPresentOrElse(
    name -> System.out.println("こんにちは、" + name + "さん"),
    () -> System.out.println("ユーザーが見つかりません")
);

ifPresentOrElse(Java 9以降)を使うと、値がある場合・無い場合の処理をそれぞれラムダ式で渡すだけで済み、get()を直接呼び出す必要が無くなります。
「Optionalを見たら、まずisPresent + getではなくorElse系やifPresentOrElseが使えないか検討する」という習慣が身につくと、コードがぐっとシンプルになります。

応用・一歩先の使い方

map・filterで値の変換とチェックを連結する

OptionalはStream APIのようにmapfilterで処理を連結できます。

Optional<String> result = findNameById(1);

String message = result
    .filter(name -> name.length() > 3) // 4文字以上のみ通過
    .map(name -> name + "さん")         // 値があれば変換
    .orElse("該当なし");

System.out.println(message); // かつコーチさん

途中で条件に合わなかったり値が無かったりした場合は、nullチェックを挟まなくても自動的にorElseの値に流れてくれます。

メソッドの引数やフィールドの型としては使わない

Optionalは便利ですが、Javaの設計者は「戻り値専用」として使うことを推奨しています。

// ❌避けるべき:フィールドや引数にOptionalを使う
public class User {
    private Optional<String> nickname; // 非推奨
}

public void greet(Optional<String> name) { // 非推奨
}

Optional自体もオブジェクトであるため、フィールドや引数に使うと、それ自体がnullになりうる(Optional型の変数にnullが入る)という本末転倒な状況を生みかねません。
フィールドや引数では、これまで通り@Nullableなどのアノテーションやnullチェックで対応し、Optionalは戻り値の型に限定して使うのが実務での定石です。

まとめ

この記事のポイント

  • Optionalは値が存在しないかもしれないことを型で表現する入れ物クラス
  • Optional.ofOptional.emptyOptional.ofNullableで生成する
  • orElseorElseGetでデフォルト値を、ifPresentOrElseで値の有無に応じた処理を書ける
  • isPresent + getばかり使うとOptional導入のメリットが薄れるため、関数的な書き方を優先する
  • Optionalは戻り値専用として使い、フィールドや引数には使わないのが定石

次に読むべき記事

  • ジェネリクスの境界(extends・super)
  • Iteratorの仕組みと拡張for文との関係

タグ: Java, 中級者向け, コレクション

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