Java Optionalクラスの実践的使い方

Javaでは、null参照に対してメソッドやフィールドにアクセスするとNullPointerExceptionが発生する。これを回避するために、従来は多数のif文でnullチェックを行う必要があり、コードの可読性と保守性が低下していた。

if (user != null) {
    System.out.println(user.getFullName());
} else {
    User defaultUser = new User("Stark", "Tony Stark");
    System.out.println(defaultUser.getFullName());
}

Optionalクラスは、このような問題を関数型スタイルで安全かつ簡潔に解決するための標準APIである。

Optionalとは何か

Optional<T>は値をラップするコンテナ型であり、「値が存在する可能性がある」または「値が存在しない(empty)」という状態を明示的に表現する。これにより、呼び出し側にnull処理を強制し、例外のリスクを減らすことができる。
また、mapfilterifPresentなどのメソッドを提供しており、命令的なnullチェックを関数型チェーンに置き換えられる。

基本的な使用方法

まず、サンプル用のクラスを定義する:

public class Member {
    private String id;
    private String displayName;

    public Member(String id, String displayName) {
        this.id = id;
        this.displayName = displayName;
    }

    public String getId() { return id; }
    public Optional<String> getDisplayName() { 
        return Optional.ofNullable(displayName); 
    }
}
public class MemberRepository {
    public Optional<Member> findById(String id) {
        if ("spider".equals(id)) {
            return Optional.of(new Member("spider", "Peter Parker"));
        }
        return Optional.empty();
    }
}

Optionalの作成

  • Optional.empty():空のOptionalを生成
  • Optional.of(value)valuenullでないことを前提にラップ(nullなら例外)
  • Optional.ofNullable(value)valuenullでも安全にラップ

値の取得とデフォルト処理

  • get():値を取得(empty時はNoSuchElementException
  • orElse(defaultValue):値があればそれを、なければ引数のデフォルト値を返す(常に評価)
  • orElseGet(Supplier<? extends T>):empty時のみSupplierを評価してデフォルト値を生成(遅延評価)
  • orElseThrow(Supplier<? extends Throwable>):empty時に指定例外をスロー
MemberRepository repo = new MemberRepository();
Optional<Member> memberOpt = repo.findById("ironman");

// 遅延評価によるデフォルト値生成
Member fallback = memberOpt.orElseGet(() -> new Member("default", "Guest User"));

// 値がなければカスタム例外をスロー
Member required = memberOpt.orElseThrow(() -> new IllegalArgumentException("Member not found"));

条件付き処理

  • ifPresent(Consumer<? super T>):値が存在する場合に処理を実行
  • ifPresentOrElse(Consumer, Runnable):値あり/なしで異なる処理を指定
  • filter(Predicate<? super T>):条件を満たせば値を保持、満たさなければemptyに
memberOpt.ifPresent(m -> System.out.println("ID: " + m.getId()));

memberOpt.ifPresentOrElse(
    m -> System.out.println("Found: " + m.getId()),
    () -> System.out.println("No member found")
);

Optional<Member> namedPeter = memberOpt.filter(m -> "Peter Parker".equals(m.getDisplayName().orElse("")));

変換操作:map vs flatMap

mapは通常の変換に使用され、戻り値がOptionalでない場合に適する。
flatMapは、変換結果がすでにOptionalである場合にネストを回避するために使用される。

// map: Optional<Member> → Optional<Optional<String>> にはならない(自動フラット化されない)
Optional<Optional<String>> displayNameNested = memberOpt.map(Member::getDisplayName);

// flatMap: Optional<Member> → Optional<String>
Optional<String> displayNameFlat = memberOpt.flatMap(Member::getDisplayName);

Streamとの連携

Optional.stream()により、OptionalStreamに変換できる。値があれば1要素のストリーム、なければ空ストリームとなる。

memberOpt.stream()
        .map(Member::getId)
        .forEach(System.out::println);

Optionalの不適切な使用例

  • フィールドとしての使用:シリアライズが複雑になり、メモリオーバーヘッドが増加
  • メソッドやコンストラクタのパラメータ:呼び出し側に余計なラッピングを強いる
  • コレクションのラッピング:空リストは既に有効な状態であり、Optional<List>は冗長
  • 無条件のget()呼び出し:安全でなく、例外リスクあり

タグ: Java Optional FunctionalProgramming

5月16日 00:48 投稿