Optionalとは、「値があるかもしれないし、ないかもしれない」ことを型で表す、Javaの標準クラスです。メソッドの戻り値を Optional<User> にすると、呼び出す側は「ないときにどうするか」を書かざるを得なくなり、nullチェックの書き忘れによる NullPointerException を減らせます。

一方で、Optionalは使い方を間違えると、かえって読みにくいコードや、思わぬ処理の実行を招きます。代表的なのが、orElse の引数が値があっても実行されるという評価タイミングの違いです。この記事では、よく使うメソッドを一通り紹介したうえで、すべてのコードをJava 21で実際に実行し、その出力を載せて解説します。使うべき場所・使わない場所の判断表と、nullチェックの入れ子を書き直す例も紹介します。

Optionalのメソッド早見表

やりたいこと メソッド 追加されたバージョン
値から作る(nullは不可) Optional.of(value) Java 8
nullかもしれない値から作る Optional.ofNullable(value) Java 8
空を作る Optional.empty() Java 8
ないときの値を指定して取り出す orElse(値) Java 8
ないときの値を、必要なときだけ作る orElseGet(() -> 値) Java 8
ないときは例外にする orElseThrow(() -> 例外) / orElseThrow() 8 / 引数なしは10
中身を変換する map(関数) Java 8
変換結果がOptionalのときに平らにする flatMap(関数) Java 8
条件に合うときだけ残す filter(条件) Java 8
あるとき・ないときで処理を分ける ifPresentOrElse(処理, ないときの処理) Java 9
ないときに別のOptionalを試す or(() -> 別のOptional) Java 9
Streamに変換する stream() Java 9
空かどうか isPresent() / isEmpty() 8 / 11

迷ったら、map で変換して、orElse・orElseGet・orElseThrow のどれかで取り出す、が基本の形です。get() は使わずに済むように書きます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Optionalの使い方|orElseとorElseGetの違いと使う場所」

この記事のサンプルと実行方法

サンプルは、ユーザーをIDで探すメソッドを中心にしています。見つからないことが普通にありうるので、戻り値をOptionalにしています。

record User(String id, String name, String email, Address address) {}
record Address(String city) {}

static Map<String, User> db = Map.of(
    "u1", new User("u1", "佐藤", "sato@example.com", new Address("東京")),
    "u2", new User("u2", "鈴木", null, null)
);

static Optional<User> findById(String id) {
    return Optional.ofNullable(db.get(id));
}

実行は、Java 21の単一ファイル実行(java OptionalDemo.java)で行いました。「u1」は全項目あり、「u2」はメールと住所がnull、「zz」は存在しないIDです。

Optionalの作り方:of・ofNullable・empty

System.out.println(Optional.of("abc"));
System.out.println(Optional.ofNullable(null));
System.out.println(Optional.empty());
Optional.of(null);
Optional[abc]
Optional.empty
Optional.empty
Optional.of(null) -> NullPointerException
メソッド 渡す値 nullを渡すと
Optional.of(value) nullでないことが確実な値 その場で NullPointerException
Optional.ofNullable(value) nullかもしれない値 空のOptionalになる
Optional.empty() なし 空のOptional

Optional.of は「nullのはずがない」ことの確認も兼ねています。nullかもしれない値(Mapの get の結果など)には、ofNullable を使います。

値の取り出し方:orElse・orElseGet・orElseThrow

System.out.println(findById("u1").map(User::name).orElse("不明"));
System.out.println(findById("zz").map(User::name).orElse("不明"));
findById("zz").orElseThrow(() -> new IllegalArgumentException("ユーザーが見つかりません: id=zz"));
findById("zz").orElseThrow();
findById("zz").get();
佐藤
不明
orElseThrow(supplier) -> ユーザーが見つかりません: id=zz
orElseThrow() -> java.util.NoSuchElementException: No value present
get() -> java.util.NoSuchElementException: No value present
メソッド 値がないとき 向いている場面
orElse(値) 指定した値を返す 既定値が定数や、すでにある変数のとき
orElseGet(() -> 値) 関数を呼んで、その結果を返す 既定値を作るのに処理が必要なとき
orElseThrow(() -> 例外) 指定した例外を投げる ないことが異常なとき(業務の例外にする)
orElseThrow() NoSuchElementException ないことがプログラムの誤りのとき
get() NoSuchElementException 使わない(orElseThrow() を使う)

get() を使わない理由

実行結果のとおり、get() と引数なしの orElseThrow() は、まったく同じ動きをします。違うのは名前だけです。get() は「必ず取れる」ように見えるため、空のときに例外になることが読み取りにくく、Javaの公式ドキュメントでもget() の代わりに orElseThrow() を使うことが推奨されています。

さらに実務では、No value present というメッセージだけでは、何が見つからなかったのかがわかりません。業務のエラーとして扱うなら、orElseThrow(() -> new UserNotFoundException(id)) のように、調査に必要な値を入れた例外にします。

orElseとorElseGetの違い:評価タイミングを実行して確認

orElse の引数は、Optionalに値があってもなくても、必ず先に実行されます。Javaのメソッド呼び出しでは、引数の値を先に計算してからメソッドを呼ぶためです。一方、orElseGet に渡すのは「値の作り方(関数)」なので、値がないときだけ実行されます。

static String loadDefault() {
    System.out.println("  loadDefault() が呼ばれた");
    return "ゲスト";
}

String a = Optional.of("佐藤").orElse(loadDefault());
String b = Optional.of("佐藤").orElseGet(OptionalDemo::loadDefault);
String c = Optional.<String>empty().orElseGet(OptionalDemo::loadDefault);
[値がある場合 orElse]
  loadDefault() が呼ばれた
  結果=佐藤
[値がある場合 orElseGet]
  結果=佐藤
[値がない場合 orElseGet]
  loadDefault() が呼ばれた
  結果=ゲスト

値が「佐藤」とわかっているのに、orElse では loadDefault() が呼ばれています。結果は同じでも、裏で余計な処理が走っています。

余計な処理が「副作用」だと、データが変わる

既定値を作る処理が、DBへの保存のような副作用(戻り値以外に、外の状態を変える処理)を持っていると、問題はもっとはっきり現れます。

User u1 = Optional.of(found).orElse(createAndSaveGuest());
User u2 = Optional.of(found).orElseGet(OptionalDemo2::createAndSaveGuest);
  ゲストを作成してDBに保存しました(1件目)
  結果=佐藤 / 保存件数=1
  結果=佐藤 / 保存件数=1

ユーザーが見つかっているのに、orElse の行でゲストが1件保存されました。orElseGet の行では、保存は行われていません。

既定値の種類 使うメソッド
定数・リテラル("不明"、0、List.of()) orElse
すでに計算済みの変数 orElse
メソッド呼び出し・new・DBやAPIへのアクセス orElseGet
副作用のある処理(保存・送信・ログ出力など) orElseGet(必須)

変換と絞り込み:map・flatMap・filter

map:中身を変換する(nullになったら空)

System.out.println(findById("u1").map(User::address).map(Address::city).orElse("住所なし"));
System.out.println(findById("u2").map(User::address).map(Address::city).orElse("住所なし"));
System.out.println(findById("u2").map(User::address));
東京
住所なし
Optional.empty

map に渡した関数がnullを返すと、結果は空のOptionalになります。そのため、user.getAddress().getCity() のような「途中がnullかもしれない」たどり方を、nullチェックなしで安全に書けます。

flatMap:Optionalを返す関数を使うとき

Optional<Optional<String>> nested = findById("u1").map(u -> Optional.ofNullable(u.email()));
Optional<String> flat = findById("u1").flatMap(u -> Optional.ofNullable(u.email()));
map で Optional を返すと: Optional[Optional[sato@example.com]]
flatMap なら: Optional[sato@example.com]

変換する関数そのものがOptionalを返すときは、flatMap を使います。map だとOptionalが二重に包まれてしまいます。Optionalを返すメソッド(findById など)をつなげるときに使います。

filter:条件に合うときだけ残す

findById("u1").map(User::email).filter(e -> e.endsWith("@example.com")).isPresent();  // true
findById("u1").map(User::email).filter(e -> e.endsWith("@example.org")).isPresent();  // false

条件に合わなければ空になります。「あるけれど使えない値」を、「ない」と同じ扱いにまとめられます。

あるとき・ないときの処理:ifPresentOrElse・or・stream

findById("u1").ifPresentOrElse(
    u -> System.out.println("見つかった: " + u.name()),
    () -> System.out.println("見つからない"));

String v = findInCache("k1").or(() -> findInDb("k1")).orElse("なし");

List<String> emails = Stream.of("u1", "u2", "zz")
    .map(OptionalDemo::findById)
    .flatMap(Optional::stream)
    .map(User::email)
    .filter(Objects::nonNull)
    .toList();
見つかった: 佐藤
  cacheを検索
  DBを検索
  結果=DBの値
[sato@example.com]
メソッド 使いどころ
ifPresent(処理) あるときだけ処理する(戻り値は不要)
ifPresentOrElse(処理, ないとき) あるとき・ないときの両方で処理する
or(() -> 別のOptional) キャッシュ→DBのように、順に探す。orElseGet と同じく、必要なときだけ実行される
stream() Streamの中で、値のあるものだけを取り出す(flatMap(Optional::stream))

or はOptionalを返すので、そのあとに map や orElse をつなげられます。最終的な既定値を決める orElse と、代わりの探し先を決める or と覚えると使い分けやすくなります。

直す前と後:よくある書き方の改善

nullチェックの入れ子 → mapのチェーン

// 直す前
static String cityBefore(User user) {
    if (user != null) {
        Address address = user.address();
        if (address != null) {
            String city = address.city();
            if (city != null) {
                return city.toUpperCase();
            }
        }
    }
    return "UNKNOWN";
}

// 直した後
static String cityAfter(User user) {
    return Optional.ofNullable(user)
        .map(User::address)
        .map(Address::city)
        .map(String::toUpperCase)
        .orElse("UNKNOWN");
}

4つのパターン(userがnull、addressがnull、cityがnull、すべてあり)で、結果が同じになることを確認しました。

  before=UNKNOWN after=UNKNOWN
  before=UNKNOWN after=UNKNOWN
  before=UNKNOWN after=UNKNOWN
  before=OSAKA after=OSAKA

isPresent() と get() の組み合わせ → map と orElse

// 直す前:nullチェックと同じ書き方になっている
String name;
if (opt.isPresent()) {
    name = opt.get().name();
} else {
    name = "不明";
}

// 直した後
String name = opt.map(User::name).orElse("不明");

isPresent() と get() の組み合わせは、nullチェックを別の名前で書いているだけです。map と orElse にすると、1行で意図が伝わり、get() の呼び忘れや確認漏れも起きません。

使うべき場所・使わない場所

Javaの公式ドキュメントには、次のように書かれています。Optionalは主に、「結果なし」を表す必要がはっきりあり、nullを使うと誤りを招きやすい場面での、メソッドの戻り値の型として使うためのものです。

使う場所 判断 理由・代わりの方法
メソッドの戻り値(見つからないことがある検索など) 使う 本来の用途。呼び出す側に「ないとき」の処理を促せる
クラスのフィールド 使わない シリアライズできない(下の実行結果)。フィールドはnullを許して、getterでOptionalを返す
メソッドの引数 使わない 呼び出す側が Optional.empty() を作る手間が増え、引数自体にnullも渡せてしまう。オーバーロードや、nullを許す引数にする
コレクション(Optional<List<T>>) 使わない 空のリストで「ない」を表せる
コレクションの要素(List<Optional<T>>) 使わない 値のあるものだけを入れる
int・long・double OptionalInt などを使う 箱詰め(ボクシング)を避けられる
必ず値があるメソッドの戻り値 使わない Optionalにすると、呼び出す側に無用な処理を強いる

フィールドに使うと、シリアライズできない

static class Holder implements Serializable {
    Optional<String> nickname = Optional.empty();
}
  java.io.NotSerializableException: java.util.Optional

Optionalは Serializable を実装していません。セッションに保存するオブジェクトや、キャッシュのようにシリアライズされるクラスのフィールドにすると、実行時にエラーになります。フィールドは普通の型のまま持ち、getterで Optional.ofNullable(nickname) を返す形にします。

Optional型の変数にnullを入れない

Optional<String> bad = null;
bad.isPresent();
Cannot invoke "java.util.Optional.isPresent()" because "<local8>" is null

公式ドキュメントでも、Optional型の変数は、それ自体をnullにしてはならず、必ずOptionalのインスタンスを指すべきとされています。Optionalを返すメソッドで return null; と書くと、呼び出す側はnullチェックとOptionalの確認の両方が必要になり、Optionalを使う意味がなくなります。ないときは Optional.empty() を返します。

※ "<local8>" と表示されているのは、変数名の情報を含めずにコンパイルされたためです。同じコードを javac -g(変数名などのデバッグ情報を含める指定)でコンパイルして実行すると、because "bad" is null と変数名が表示されることを確認しました。

数値にはOptionalInt

OptionalInt max = IntStream.of().max();
System.out.println(max + " " + max.orElse(0));
System.out.println(IntStream.of(3, 9, 4).max());
OptionalInt.empty 0
OptionalInt[9]

IntStream の max() や average() は、要素が0件のこともあるため、OptionalInt・OptionalDouble を返します。

よくある間違いのチェックリスト

チェック項目 直し方
get() を使っている orElseThrow()、または map + orElse
isPresent() の直後に get() している map・ifPresent・orElse にまとめる
orElse(メソッド呼び出し) になっている orElseGet(() -> ...) にする
Optionalを返すメソッドで return null return Optional.empty()
Optional.of(nullかもしれない値) Optional.ofNullable
map の中でOptionalを返して二重になっている flatMap
フィールド・引数・コレクションにOptional 普通の型・空のコレクションにする
orElseThrow() の例外に情報がない orElseThrow(() -> new XxxException(id))
Optional同士を == で比べている equals で比べる(Optionalは値ベースのクラス)

よくある質問

Q. Optionalを使えば、NullPointerExceptionは起きなくなりますか? A. なくなるわけではありません。Optionalは「ないかもしれない」ことを型で伝えるための仕組みで、get() を使ったり、Optional型の変数にnullを入れたりすれば、例外は起きます。NullPointerExceptionの原因の調べ方は、同シリーズの「NullPointerExceptionの原因と対処」で解説しています。

Q. orElse(null) は使ってもいいですか? A. Optionalを使わない古いコードやライブラリにnullを渡す必要がある、境界の部分では使います。自分のコードの中では、Optionalのまま map などでつなげる方が、nullを再び持ち込まずに済みます。

Q. Spring Data JPAの findById がOptionalを返すのはなぜですか? A. IDで探しても見つからないことが普通にありうるためで、まさにOptionalの本来の用途です。サービス層で findById(id).orElseThrow(() -> new OrderNotFoundException(id)) のように、「ないときにどうするか」を決めます。

Q. isEmpty() と !isPresent() は違いますか? A. 結果は同じです。Java 11で追加された isEmpty() の方が、否定の ! を読まずに済むぶん、意図が伝わりやすくなります。

まとめ

  • Optionalは「ないかもしれない」を型で表すもの。主な用途はメソッドの戻り値
  • 作るときは、nullかもしれない値なら ofNullable、ないときは Optional.empty()(nullを返さない)
  • 取り出しは orElse・orElseGet・orElseThrow。get() は使わず、例外には調査に必要な値を入れる
  • orElse の引数は、値があっても実行される(実行結果で確認)。処理が必要な既定値は orElseGet
  • 変換は map、Optionalを返す関数なら flatMap、代わりの探し先は or
  • フィールド・引数・コレクションには使わない。フィールドはシリアライズできず、コレクションは空で表せる
  • isPresent() + get() や、nullチェックの入れ子は、map + orElse のチェーンに書き直せる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Optionalの使い方|orElseとorElseGetの違いと使う場所」

株式会社RJCでは、読みやすく安全なJavaのコードの書き方を、日々のレビューの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?