SJ blog
Java
Z

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

JavaのOptionalを正しく使う — アンチパターンと実践ガイド

Optional の正しい使い方と、isPresent()+get()を使うアンチパターン、map/flatMap/orElseGet/ifPresentOrElseの使い方を解説します。

一言結論

OptionalはisPresent()+get()の組み合わせで使うと従来のnullチェックと変わらず、map/flatMap/orElseGetのメソッドチェーンで使ってこそ「null安全な変換パイプライン」という本来の価値が発揮される。

Optional とは

null を返すかもしれないメソッドの返り値をラップするコンテナです。Java 8 で導入されました。

// null を返す可能性があるメソッド(旧来のやり方)
public User findUser(int id) {
    return database.find(id);  // 見つからなければ null
}

// Optional を使う(推奨)
public Optional<User> findUser(int id) {
    return Optional.ofNullable(database.find(id));
}

Optional の作り方

// 値あり
Optional<String> opt = Optional.of("hello");

// null かもしれない値をラップ
Optional<String> opt = Optional.ofNullable(value);  // null なら empty

// 空の Optional
Optional<String> empty = Optional.empty();

よくあるアンチパターン

NG1: isPresent() + get() の組み合わせ

// NG: Optional の意味がない
Optional<User> userOpt = findUser(id);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    System.out.println(user.getName());
}

// OK: ifPresent を使う
findUser(id).ifPresent(user -> System.out.println(user.getName()));

NG2: Optional を引数に使う

// NG: 引数を Optional にしない
public void process(Optional<String> name) { ... }

// OK: 引数はnullチェックかオーバーロードで対応
public void process(String name) {
    Objects.requireNonNull(name);
    // ...
}

NG3: Optional をフィールドに持つ

// NG: シリアライズで問題が起きる
public class User {
    private Optional<String> email;  // NG
}

// OK: @Nullable アノテーションを使う
public class User {
    private @Nullable String email;
}

NG4: コレクションを Optional で返す

// NG: 空コレクションを返せばよい
public Optional<List<User>> findAll() { ... }

// OK: 空のリストを返す
public List<User> findAll() {
    return Collections.emptyList();
}

正しい使い方

orElse / orElseGet / orElseThrow

// orElse: デフォルト値(常に評価される)
String name = findUser(id).map(User::getName).orElse("Anonymous");

// orElseGet: デフォルト値を遅延評価(Supplier で渡す)
String name = findUser(id).map(User::getName)
    .orElseGet(() -> generateDefaultName());  // 必要な時だけ実行

// orElseThrow: 値がなければ例外をスロー(Java 10以降引数なしも可)
User user = findUser(id).orElseThrow(
    () -> new UserNotFoundException("User not found: " + id)
);

// Java 10以降: 引数なしは NoSuchElementException
User user = findUser(id).orElseThrow();

orElse vs orElseGet:
デフォルト値の生成コストが高い場合は orElseGet を使います。orElse("定数") のように定数ならどちらでも同じです。

map / flatMap / filter

// map: 値があれば変換する
Optional<String> name = findUser(id).map(User::getName);

// 複数の変換をチェーンする
Optional<String> city = findUser(id)
    .map(User::getAddress)    // User → Address
    .map(Address::getCity);   // Address → String

// flatMap: Optional を返すメソッドをチェーン
// map だと Optional<Optional<Address>> になるが flatMap だと Optional<Address>
Optional<String> city = findUser(id)
    .flatMap(user -> user.getOptionalAddress())  // Optional<Address> を返す
    .map(Address::getCity);

// filter: 条件を満たす場合だけ値を残す
Optional<User> adultUser = findUser(id)
    .filter(user -> user.getAge() >= 18);

ifPresent / ifPresentOrElse

// ifPresent: 値がある場合だけ処理
findUser(id).ifPresent(user -> sendWelcomeEmail(user));

// ifPresentOrElse(Java 9以降): ある場合とない場合で分岐
findUser(id).ifPresentOrElse(
    user -> sendWelcomeEmail(user),
    () -> log.warn("User not found: " + id)
);

or(Java 9以降)

// Optional が空の場合に別の Optional を返す
Optional<User> user = findInLocalCache(id)
    .or(() -> findInDatabase(id))
    .or(() -> findInRemote(id));

stream(Java 9以降)

// Optional を Stream に変換(値があれば1要素、なければ0要素)
List<User> users = userIds.stream()
    .map(this::findUser)       // Stream<Optional<User>>
    .flatMap(Optional::stream) // Stream<User>(空のOptionalは除外)
    .collect(Collectors.toList());

まとめ

状況使うメソッド
デフォルト値を返したいorElse / orElseGet
例外を投げたいorElseThrow
変換したいmap / flatMap
条件でフィルタfilter
副作用の処理ifPresent / ifPresentOrElse
ストリームで処理stream()

Optional は「返り値の型」として使うのが正しい使い方です。フィールドや引数には使わず、メソッドの返り値の型として使いましょう。