Java 8で導入されたOptionalは、NullPointerException(NPE)を防ぎ、コードの可読性を向上させるための強力なツールです。しかし、Optionalが空だった場合の処理をどうするかは、開発者にとって悩ましい問題です。今回は、Optionalの終着点とも言える`orElse()`, `orElseGet()`, `orElseThrow()`の3つのメソッドに焦点を当て、それぞれの特徴、使い分け、そして現場で役立つ注意点について、シニアJavaエンジニアの視点から解説します。
1. Optionalの基本とNPE回避の重要性
`Optional`は、値が存在するかもしれないし、しないかもしれない値を安全に扱うためのコンテナです。これにより、メソッドの戻り値で`null`を返す代わりに`Optional.empty()`を返すことができ、呼び出し元は値の存在を明示的にチェックするようになります。
例えば、以下のようなコードはNPEを引き起こす可能性があります。
String name = user.getName(); // userがnullの場合、あるいはuser.getName()がnullを返す場合
if (name.length() > 0) {
System.out.println(“名前の長さ: ” + name.length());
}
`Optional`を使うと、より安全に書くことができます。
Optional
Optional
// ここからが本題です! nameOptionalが空だった場合の処理をどうするか?
2. Optionalの終着点:orElse(), orElseGet(), orElseThrow()
Optionalが空だった場合に、どのような値を返すか、あるいはどのような例外をスローするかを決定するのが、今回解説する3つのメソッドです。
2.1. `orElse(T other)`
`orElse()`メソッドは、Optionalに値が存在しない場合に、引数で渡されたデフォルト値を返します。
特徴:
- 常にデフォルト値の評価が行われます。
- シンプルで直感的に使えます。
利用シーン:
- デフォルト値の計算コストが非常に低い場合。
- 常に同じデフォルト値を返したい場合。
2.2. `orElseGet(Supplier
`orElseGet()`メソッドは、Optionalに値が存在しない場合に、引数で渡された`Supplier`(値を生成する関数)を実行し、その結果を返します。
特徴:
- Optionalに値が存在する場合、`Supplier`は実行されません。
- デフォルト値の生成にコストがかかる場合や、動的にデフォルト値を生成したい場合に有効です。
利用シーン:
- デフォルト値の生成に時間がかかる場合(DBアクセス、複雑な計算など)。
- 実行時に異なるデフォルト値を生成したい場合。
2.3. `orElseThrow(Supplier
`orElseThrow()`メソッドは、Optionalに値が存在しない場合に、引数で渡された`Supplier`を実行し、そこで生成された例外をスローします。
特徴:
- 値が存在しないことが異常な状態である場合に、明確なエラーハンドリングを強制できます。
- `orElseThrow()`には、例外クラスを指定するオーバーロードもあります(例: `orElseThrow(IllegalStateException::new)`)。
利用シーン:
- 値が存在しないことがアプリケーションのロジックとして許容されない場合。
- NPEを避けるために、より具体的な例外で処理を中断したい場合。
3. 実装例とサンプルコード
それでは、それぞれのメソッドを使った具体的なコード例を見ていきましょう。
シナリオ: ユーザーIDからユーザー情報を取得するサービスがあり、ユーザーが存在しない場合はデフォルトのユーザー名を表示するか、エラーとする。
import java.util.Optional;
import java.util.function.Supplier;
// ユーザーを表すシンプルなクラス
class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// ユーザー取得サービス(モック)
class UserService {
// ID 101 のユーザーは存在するが、ID 999 は存在しないと仮定
public Optional
if (id == 101) {
return Optional.of(new User(“Alice”));
} else {
return Optional.empty(); // ユーザーが見つからない場合は Optional.empty() を返す
}
}
}
public class OptionalDemo {
public static void main(String[] args) {
UserService userService = new UserService();
// — orElse() の例 —
System.out.println(“— orElse() の例 —“);
int userId1 = 101;
User user1 = userService.findUserById(userId1).orElse(new User(“Guest”)); // ユーザーが存在するので “Alice” が取得される
System.out.println(“ユーザー名 (ID: ” + userId1 + “): ” + user1.getName());
int userId2 = 999;
User user2 = userService.findUserById(userId2).orElse(new User(“Guest”)); // ユーザーが存在しないので “Guest” が返される
System.out.println(“ユーザー名 (ID: ” + userId2 + “): ” + user2.getName());
// 注意: orElse() では、デフォルト値の生成は常に評価される
User defaultUser = new User(“Default”);
User user3 = userService.findUserById(userId1).orElse(defaultUser); // defaultUser は評価されるが、使われない
System.out.println(“orElse() で常に評価されるデフォルト値の例 (ID: ” + userId1 + “): ” + user3.getName());
// defaultUser オブジェクトは、ユーザーが見つかった場合でも生成されている点に注意
System.out.println(“\n”);
// — orElseGet() の例 —
System.out.println(“— orElseGet() の例 —“);
int userId3 = 101;
// Supplier を使ってデフォルトユーザーを生成
Supplier
System.out.println(“デフォルトユーザーを生成中…”); // このログは userId3=101 の場合は表示されない
return new User(“Guest”);
};
User user4 = userService.findUserById(userId3).orElseGet(defaultUserSupplier);
System.out.println(“ユーザー名 (ID: ” + userId3 + “): ” + user4.getName());
int userId4 = 999;
User user5 = userService.findUserById(userId4).orElseGet(defaultUserSupplier); // ユーザーが存在しないので Supplier が実行され “Guest” が返される
System.out.println(“ユーザー名 (ID: ” + userId4 + “): ” + user5.getName());
System.out.println(“\n”);
// — orElseThrow() の例 —
System.out.println(“— orElseThrow() の例 —“);
int userId5 = 101;
try {
User user6 = userService.findUserById(userId5)
.orElseThrow(() -> new IllegalStateException(“ユーザーが見つかりませんでした! (ID: ” + userId5 + “)”));
System.out.println(“ユーザー名 (ID: ” + userId5 + “): ” + user6.getName());
} catch (IllegalStateException e) {
System.err.println(“エラー: ” + e.getMessage());
}
int userId6 = 999;
try {
// ユーザーが存在しないので、例外がスローされる
User user7 = userService.findUserById(userId6)
.orElseThrow(() -> new IllegalStateException(“ユーザーが見つかりませんでした! (ID: ” + userId6 + “)”));
System.out.println(“ユーザー名 (ID: ” + userId6 + “): ” + user7.getName()); // この行は実行されない
} catch (IllegalStateException e) {
System.err.println(“エラー: ” + e.getMessage()); // ここで例外がキャッチされる
}
// 例外クラスを指定するオーバーロードの例
int userId7 = 999;
try {
userService.findUserById(userId7)
.orElseThrow(IllegalArgumentException::new); // 例外クラスのみ指定
} catch (IllegalArgumentException e) {
System.err.println(“IllegalArgumentException がスローされました。”);
}
}
}
4. 応用と注意点
4.1. `orElse()` vs `orElseGet()` のパフォーマンス
`orElse()`は、Optionalに値があってもなくても、常に引数の評価を行います。一方、`orElseGet()`はOptionalに値があれば引数の評価をスキップします。
例えば、デフォルト値の生成に時間がかかる場合(DBアクセス、複雑な計算など)は、`orElseGet()`を使用しないと、不要な処理が実行されてパフォーマンスの低下を招く可能性があります。
悪い例(`orElse()`でコストの高い処理を呼ぶ):
String defaultName = expensiveOperationToGetDefaultName(); // ユーザーがいなくても常に実行される
String name = optionalName.orElse(defaultName);
良い例(`orElseGet()`でコストの高い処理を呼ぶ):
String name = optionalName.orElseGet(() -> expensiveOperationToGetDefaultName()); // ユーザーがいない場合のみ実行される
4.2. `orElseThrow()` で適切な例外をスローする
`orElseThrow()`は、値が存在しないことが「エラー」である場合に非常に強力です。しかし、どのような例外をスローするかは慎重に検討する必要があります。
- `NullPointerException`: `Optional.get()` は NPE をスローしますが、これは避けるべきです。
- `IllegalStateException`: 値が存在しないことが、現在の状態では予期しない、あるいは不正な場合に使用します。
- `IllegalArgumentException`: メソッドの引数として渡された値に関連して、不正な状態を表現する場合に使用します。
- カスタム例外: アプリケーション固有のエラーを表現するために、独自の例外クラスを定義してスローすることも推奨されます。
4.3. `Optional` の連鎖と `orElse()` ファミリー
`Optional`を連鎖させる場合、最終的に値を取り出す、あるいはデフォルト値を設定する、例外をスローする、といういずれかの処理でOptionalのチェーンを終える必要があります。
Optional
Optional address = user.flatMap(User::getAddress); // flatMap を使ってネストした Optional を平坦化
// ここで orElse, orElseGet, orElseThrow を使って処理を確定させる
String street = address.map(Address::getStreet) // Address から Street を Optional
.orElse(“不明な住所”); // Street がない場合は “不明な住所” を返す
まとめ
- `Optional.orElse(T other)`: 値がない場合に、常に評価されるデフォルト値を返します。
- `Optional.orElseGet(Supplier extends T> other)`: 値がない場合に、`Supplier`を実行してデフォルト値を生成します。コストの高いデフォルト値生成に適しています。
- `Optional.orElseThrow(Supplier extends X> exceptionSupplier)`: 値がない場合に、`Supplier`を実行して例外をスローします。値の不存在がエラー状態である場合に適しています。
これらのメソッドを適切に使い分けることで、NullPointerExceptionを効果的に回避し、より安全で保守性の高いJavaコードを書くことができます。特に、デフォルト値の生成コストや、値が存在しないことのビジネスロジック上の意味合いを考慮して、最適なメソッドを選択してください。

コメント