
はじめに
nullチェックだらけのコードに悩んでいませんか?
Java 8 で導入された は、nullを安全に扱い、コードをより明確にするための強力な仕組みです。Optional
しかし、Optional は「便利そうだから」といって無闇に使うと、逆にコードが読みにくくなる落とし穴もあります。
この記事では、Optionalの基本的な使い方から、実務での正しい設計指針・避けるべきアンチパターンまでを丁寧に解説します。
Optionalとは?
Optional<T> は、「値が存在するかもしれない」「存在しないかもしれない」ことを明示的に表現するコンテナクラスで、つまり「null かもしれない」を型で表現する仕組みです。
Optional<String> name = Optional.of("Alice");
System.out.println(name.get()); // Alice
Optional は null の代わりに「空の箱(Empty)」を返すことで、NPE の発生を防ぎ、コードをより安全かつ明示的にします。
Optionalの基本的な生成方法
| メソッド | 用途 | 例 |
|---|---|---|
Optional.of(value) | 値が絶対に非nullの場合 | Optional.of("Hello") |
Optional.ofNullable(value) | 値がnullかもしれない場合 | Optional.ofNullable(name) |
Optional.empty() | 空のOptionalを生成 | Optional.empty() |
値の取得方法
① get()(❌非推奨)
最初から非推奨例になりますが、こちらはOptionalの利点が全く利用できない取得方法になります。
Optional<String> name = Optional.ofNullable(null);
System.out.println(name.get()); // ❌ NoSuchElementException
get() は値が存在しないと例外を投げるため、実務では基本的に使いません。
② orElse():デフォルト値を返す
String name = Optional.ofNullable(null).orElse("ゲスト");
System.out.println(name); // ゲスト
値が存在しない場合に、指定したデフォルト値を返します。
③ orElseGet():遅延評価できるデフォルト値
String name = Optional.ofNullable(null)
.orElseGet(() -> computeDefaultName());
orElse() と違い、必要なときだけ関数が呼ばれるため、重い処理を含むときはこちらが有利です。
④ orElseThrow():存在しないときに例外を投げる
String name = Optional.ofNullable(null)
.orElseThrow(() -> new IllegalArgumentException("名前が見つかりません"));
「存在しないなら明確にエラーにしたい」というケースで使います。
条件分岐と処理連鎖
① isPresent() / ifPresent()
Optional<String> name = Optional.ofNullable("Alice");
if (name.isPresent()) {
System.out.println(name.get());
}
// よりモダンな書き方
name.ifPresent(n -> System.out.println("Hello, " + n));
② map():中の値を変換する
Optional<String> name = Optional.of("alice");
Optional<String> upper = name.map(String::toUpperCase);
System.out.println(upper.get()); // ALICE
値が存在する場合のみ変換処理を行い、存在しない場合は空のOptionalを返します。
実務での設計指針
メソッドの戻り値にはOptionalを使う
public Optional<User> findUserById(String id) {
return Optional.ofNullable(repository.find(id));
}
「存在しないことがあり得る」戻り値をOptionalで表現するのは非常に有効です。呼び出し側はnullチェックではなく orElse() などで安全に扱えます。
引数にOptionalを使うのはNG
// ❌ 悪い例
public void sendMessage(Optional<String> message) { ... }
Optionalは「戻り値のnull回避」を目的として設計されており、引数に使うのは想定外(アンチパターン) です。
フィールドにOptionalを使うのもNG
// ❌ 悪い例
class User {
private Optional<String> email; // アンチパターン
}
フィールドがOptionalになると、シリアライズやORM(例:JPA, MyBatis)で問題を起こします。
フィールドはnullableにして、取り出すときにOptionalへ変換するのが正解です。
よくあるアンチパターンまとめ
| アンチパターン | 問題点 | 正しい書き方 |
|---|---|---|
if (optional.isPresent()) { optional.get() } | Optionalの利点を潰している | optional.ifPresent() |
Optional.of(null) | NullPointerExceptionを投げる | Optional.ofNullable() |
| メンバー変数にOptional | シリアライズ不具合・可読性低下 | フィールドは普通の型に |
| メソッド引数にOptional | 想定外の設計 | Optionalは戻り値専用 |
Streamとの組み合わせ
Optionalは単体でも便利ですが、Stream API と組み合わせると、さらに強力で安全なnull回避コードが書けます。
特に map や flatMap を活用することで、「ネストしたnullチェック」を完全に排除できます。
StreamAPIに関する記事は、こちらにも記載していますので、併せて確認してみてください。
基本:Optionalのmap/flatMapを使った変換
Optional<User> user = findUser(); // Optional<User> 型
Optional<String> email = user.map(User::getEmail);
map() は「中の値が存在する場合のみ」関数を実行し、存在しない場合は空の Optional を返します。
もし User::getEmail 自体が Optional<String> を返す場合は、flatMap() を使ってネストを回避します。
Optional<User> user = findUser();
Optional<String> email = user.flatMap(User::getEmail); // Optional<Optional<String>> にならない
実践例①:ネストした null チェックを廃止する
以下のようなコード、見覚えはありませんか?
if (user != null && user.getAddress() != null && user.getAddress().getZipCode() != null) {
System.out.println(user.getAddress().getZipCode());
}
Optionalを使えば、これをたった数行で安全に書けます。
Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getZipCode)
.ifPresent(System.out::println);
何が起こっているかというと、
で、これこそが 「null安全な関数型スタイル」 でモダンJavaの記述です。
実践例②:Stream × Optional × flatMap
Java 9 以降では Optional::stream が導入され、Optional をそのまま Stream に変換して流せるようになりました。たとえば「ユーザー一覧から、登録済みメールアドレスをすべて取り出す」ケースを考えます。
List<User> users = List.of(
new User("Alice", Optional.of("alice@example.com")),
new User("Bob", Optional.empty()),
new User("Charlie", Optional.of("charlie@example.com"))
);
List<String> emails = users.stream()
.map(User::getEmail) // Stream<Optional<String>>
.flatMap(Optional::stream) // Stream<String> に変換
.collect(Collectors.toList());
System.out.println(emails); // [alice@example.com, charlie@example.com]
ここでのポイントは flatMap(Optional::stream)。
空の Optional は自動的に無視され、「存在する値だけ」 を安全に抽出できます。
実践例③:Streamチェーンで安全に値を取り出す
もう少し複雑な例を見てみましょう。
ユーザー → 注文 → 配送先住所 → 郵便番号 のような多段構造です。
Optional<String> zipCode = users.stream()
.findFirst() // Optional<User>
.flatMap(User::getOrder) // Optional<Order>
.flatMap(Order::getDeliveryAddress) // Optional<Address>
.map(Address::getZipCode); // Optional<String>
zipCode.ifPresent(System.out::println);
途中のどの段階で null が出ても安全にスキップされ、結果として Optional<String> が得られます。
これにより、ネストした if != null チェックを完全に排除できます。
応用:OptionalをStreamパイプラインに自然に統合
List<String> zipCodes = users.stream()
.map(User::getOrder)
.flatMap(Optional::stream) // Orderだけを抽出
.map(Order::getDeliveryAddress)
.flatMap(Optional::stream) // Addressだけを抽出
.map(Address::getZipCode)
.filter(Objects::nonNull)
.collect(Collectors.toList());
このように、OptionalとStreamを組み合わせることで、nullセーフでクリーンなデータ変換パイプラインを実現できます。
まとめ
Optional は単なる「null回避ツール」ではなく、「値が存在する/しない」を明確に表現する設計ツール です。無闇に使うのではなく、「戻り値に限定して使う」「チェーン処理で安全に扱う」ことを意識すれば、モダンですっきりした設計・実装になるはずです!ぜひコードに取り入れてみてください!
