はじめに
Javaで最も有名なエラーといえばNullPointerExceptionです。
String name = null;
System.out.println(name.length()); // NullPointerException!
Java 8で導入された**Optional**は、「値があるかもしれないし、ないかもしれない」ことを型で明示し、nullを安全に扱うための仕組みです。
この記事ではOptionalの基本から、Spring Bootでよく見るorElseThrowまで段階的に解説します。
Optionalとは
Optional<T>は、「値が入っているかもしれない箱」です。
Optional<String>
├── 値あり → "太郎" が入っている
└── 値なし → 空(nullではない!)
nullとの違い
// nullの場合:使う側が常にnullチェックしないと危険
String name = findName(); // nullかもしれない
name.length(); // NullPointerExceptionの可能性!
// Optionalの場合:型が「ないかもしれない」ことを教えてくれる
Optional<String> name = findName(); // 「ないかもしれない」と型で明示
// name.length(); // コンパイルエラー!直接使えない → 安全
Optionalの生成方法
import java.util.Optional;
public class OptionalCreate {
public static void main(String[] args) {
// 1. 値がある場合
Optional<String> opt1 = Optional.of("Hello");
System.out.println(opt1); // Optional[Hello]
// 2. 空のOptional
Optional<String> opt2 = Optional.empty();
System.out.println(opt2); // Optional.empty
// 3. nullかもしれない値を包む(最もよく使う)
String value = null;
Optional<String> opt3 = Optional.ofNullable(value);
System.out.println(opt3); // Optional.empty
String value2 = "World";
Optional<String> opt4 = Optional.ofNullable(value2);
System.out.println(opt4); // Optional[World]
}
}
| メソッド | 用途 | nullを渡すと |
|---|---|---|
Optional.of(値) |
値が絶対にある場合 | NullPointerException |
Optional.empty() |
空のOptionalを作る | — |
Optional.ofNullable(値) |
nullかもしれない場合 |
Optional.empty()になる |
Optionalから値を取り出す
isPresent + get(非推奨パターン)
import java.util.Optional;
public class IsPresentDemo {
public static void main(String[] args) {
Optional<String> opt = Optional.of("Hello");
// 動くが、nullチェックと変わらない → 非推奨
if (opt.isPresent()) {
System.out.println(opt.get()); // Hello
}
}
}
これではOptionalを使う意味がありません。以下のメソッドを使いましょう。
orElse:値がなければデフォルト値
import java.util.Optional;
public class OrElseDemo {
public static void main(String[] args) {
Optional<String> opt1 = Optional.of("太郎");
Optional<String> opt2 = Optional.empty();
System.out.println(opt1.orElse("名無し")); // 太郎
System.out.println(opt2.orElse("名無し")); // 名無し
}
}
orElseGet:値がなければ処理を実行してデフォルト値を生成
import java.util.Optional;
public class OrElseGetDemo {
public static void main(String[] args) {
Optional<String> opt = Optional.empty();
// Supplierで遅延評価(必要な時だけ実行される)
String result = opt.orElseGet(() -> "デフォルト値を生成");
System.out.println(result); // デフォルト値を生成
}
}
orElseとorElseGetの違い
import java.util.Optional;
public class OrElseVsOrElseGet {
public static String createDefault() {
System.out.println("createDefault() が呼ばれた!");
return "デフォルト";
}
public static void main(String[] args) {
Optional<String> opt = Optional.of("太郎"); // 値あり
// orElse: 値があっても createDefault() が実行される
String r1 = opt.orElse(createDefault());
// 出力: createDefault() が呼ばれた!
// orElseGet: 値があれば createDefault() は実行されない
String r2 = opt.orElseGet(() -> createDefault());
// 出力: (なし)
System.out.println(r1); // 太郎
System.out.println(r2); // 太郎
}
}
DB問い合わせなど重い処理をデフォルト値にする場合は、orElseGetを使うべきです。
orElseThrow:値がなければ例外をスロー
ここが本題です。Spring Bootで最も頻出するOptionalメソッドです。
基本
import java.util.Optional;
public class OrElseThrowDemo {
public static void main(String[] args) {
Optional<String> opt1 = Optional.of("太郎");
Optional<String> opt2 = Optional.empty();
// 値あり → そのまま返す
String name1 = opt1.orElseThrow(() -> new RuntimeException("見つかりません"));
System.out.println(name1); // 太郎
// 値なし → 例外をスロー
String name2 = opt2.orElseThrow(() -> new RuntimeException("見つかりません"));
// RuntimeException: 見つかりません
}
}
orElseThrowの仕組み
orElseThrowの中身は、概念的にはこうなっています。
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (値がある) {
return 値;
} else {
throw exceptionSupplier.get(); // Supplierから例外を取得してスロー
}
}
引数の() -> new RuntimeException(...)は、例外を生成するSupplier(ラムダ式)です。
// この部分を分解すると
.orElseThrow(() -> new RuntimeException("not found"))
// こういうこと
Supplier<RuntimeException> supplier = () -> new RuntimeException("not found");
.orElseThrow(supplier)
値がある場合、Supplierは実行されません(例外オブジェクトは作られない)。これが効率的なポイントです。
引数なしのorElseThrow(Java 10以降)
// 引数なしだと NoSuchElementException がスローされる
String name = opt.orElseThrow();
// NoSuchElementException: No value present
Spring BootでのorElseThrow実践パターン
基本パターン
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
public Todo findById(Long id) {
return todoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Todo not found: id " + id));
}
}
findByIdの戻り値はOptional<Todo>なので、orElseThrowで安全に取り出します。
カスタム例外を使うパターン(実務向け)
// カスタム例外
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(resource + " not found: id " + id);
}
}
// Service
public Todo findById(Long id) {
return todoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Todo", id));
}
カスタム例外にしておくと、例外ハンドラーで統一的にエラーレスポンスを返せます。
Optionalのその他の便利メソッド
map:値があれば変換する
import java.util.Optional;
public class OptionalMapDemo {
public static void main(String[] args) {
Optional<String> opt = Optional.of("Hello");
// 値があれば大文字に変換、なければ空のまま
Optional<String> upper = opt.map(s -> s.toUpperCase());
System.out.println(upper); // Optional[HELLO]
}
}
ifPresent:値があれば処理を実行
import java.util.Optional;
public class IfPresentDemo {
public static void main(String[] args) {
Optional<String> opt = Optional.of("太郎");
// 値がある場合のみ実行(なければ何もしない)
opt.ifPresent(name -> System.out.println("名前: " + name));
// 名前: 太郎
}
}
filter:条件に合えば値を保持、合わなければ空に
import java.util.Optional;
public class OptionalFilterDemo {
public static void main(String[] args) {
Optional<Integer> opt = Optional.of(20);
Optional<Integer> adult = opt.filter(age -> age >= 18);
Optional<Integer> child = opt.filter(age -> age < 18);
System.out.println(adult); // Optional[20]
System.out.println(child); // Optional.empty
}
}
メソッドの使い分け早見表
| やりたいこと | メソッド |
|---|---|
| 値がなければデフォルト値を返す | orElse("default") |
| 値がなければ処理を実行してデフォルト値を返す | orElseGet(() -> ...) |
| 値がなければ例外をスロー | orElseThrow(() -> ...) |
| 値があれば変換する | map(v -> ...) |
| 値があれば処理を実行 | ifPresent(v -> ...) |
| 値を条件でフィルタ | filter(v -> ...) |
まとめ
| 概念 | 意味 |
|---|---|
Optional<T> |
「値があるかもしれないし、ないかもしれない」を表す型 |
Optional.of(値) |
値が確実にある場合に使う |
Optional.ofNullable(値) |
nullかもしれない値を包む |
Optional.empty() |
空のOptionalを生成 |
orElse(デフォルト値) |
値がなければデフォルト値 |
orElseThrow(Supplier) |
値がなければ例外スロー |
| Spring Bootでの頻出 | findById(id).orElseThrow(() -> new 例外(...)) |
Optional + orElseThrowは、Spring Bootでのデータ取得の定番パターンです。「IDで検索して、なければ例外」を安全かつ簡潔に書けます。
演習問題
問題1 ⭐(基本)
以下のnullチェックをOptionalを使って書き換えてください。
public class Exercise1 {
public static String greet(String name) {
if (name != null) {
return "Hello, " + name + "!";
} else {
return "Hello, Guest!";
}
}
public static void main(String[] args) {
System.out.println(greet("太郎")); // Hello, 太郎!
System.out.println(greet(null)); // Hello, Guest!
}
}
模範解答
import java.util.Optional;
public class Exercise1 {
public static String greet(String name) {
return Optional.ofNullable(name)
.map(n -> "Hello, " + n + "!")
.orElse("Hello, Guest!");
}
public static void main(String[] args) {
System.out.println(greet("太郎")); // Hello, 太郎!
System.out.println(greet(null)); // Hello, Guest!
}
}
-
Optional.ofNullable(name):nullかもしれない値をOptionalで包む -
.map(n -> ...):値があればメッセージに変換 -
.orElse(...):値がなければデフォルトのメッセージ
問題2 ⭐⭐(応用)
以下のコードでfindUserがOptional<String>を返すように変更し、mainメソッドではorElseThrowを使って値を取り出してください。
import java.util.HashMap;
import java.util.Map;
public class Exercise2 {
private static Map<Integer, String> users = new HashMap<>(Map.of(
1, "太郎",
2, "花子",
3, "次郎"
));
public static String findUser(int id) {
return users.get(id); // nullが返る可能性あり
}
public static void main(String[] args) {
System.out.println(findUser(1)); // 太郎
System.out.println(findUser(99)); // null(危険!)
}
}
模範解答
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class Exercise2 {
private static Map<Integer, String> users = new HashMap<>(Map.of(
1, "太郎",
2, "花子",
3, "次郎"
));
public static Optional<String> findUser(int id) {
return Optional.ofNullable(users.get(id));
}
public static void main(String[] args) {
// 存在するID → 値が返る
String user1 = findUser(1)
.orElseThrow(() -> new RuntimeException("User not found: id 1"));
System.out.println(user1); // 太郎
// 存在しないID → 例外がスローされる
String user99 = findUser(99)
.orElseThrow(() -> new RuntimeException("User not found: id 99"));
// RuntimeException: User not found: id 99
}
}
findUserの戻り値をOptional<String>にすることで、呼び出し側にnullの可能性を明示できます。
問題3 ⭐⭐⭐(チャレンジ)
以下の要件を満たすProductServiceを作成してください。
-
findById(Long id): IDで商品を検索。なければProductNotFoundExceptionをスロー -
findByName(String name): 名前で商品を検索。なければ"不明な商品"を返す -
findExpensiveProduct(Long id, int threshold): IDで検索し、価格がthreshold以上ならその商品名を返す。商品がないか条件に合わなければ"該当なし"を返す
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
class Product {
String name;
int price;
Product(String name, int price) { this.name = name; this.price = price; }
}
class ProductNotFoundException extends RuntimeException {
ProductNotFoundException(Long id) { super("Product not found: id " + id); }
}
public class ProductService {
private Map<Long, Product> products = new HashMap<>(Map.of(
1L, new Product("ノートPC", 150000),
2L, new Product("マウス", 3000),
3L, new Product("キーボード", 12000)
));
// ここに3つのメソッドを実装
public static void main(String[] args) {
ProductService service = new ProductService();
// findById
System.out.println(service.findById(1L).name); // ノートPC
// service.findById(99L); // ProductNotFoundException
// findByName
System.out.println(service.findByName("マウス")); // マウス
System.out.println(service.findByName("タブレット")); // 不明な商品
// findExpensiveProduct
System.out.println(service.findExpensiveProduct(1L, 100000)); // ノートPC
System.out.println(service.findExpensiveProduct(2L, 100000)); // 該当なし
System.out.println(service.findExpensiveProduct(99L, 100000)); // 該当なし
}
}
模範解答
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
class Product {
String name;
int price;
Product(String name, int price) { this.name = name; this.price = price; }
}
class ProductNotFoundException extends RuntimeException {
ProductNotFoundException(Long id) { super("Product not found: id " + id); }
}
public class ProductService {
private Map<Long, Product> products = new HashMap<>(Map.of(
1L, new Product("ノートPC", 150000),
2L, new Product("マウス", 3000),
3L, new Product("キーボード", 12000)
));
// 値がなければ例外
public Product findById(Long id) {
return Optional.ofNullable(products.get(id))
.orElseThrow(() -> new ProductNotFoundException(id));
}
// 値がなければデフォルト値
public String findByName(String name) {
return products.values().stream()
.filter(p -> p.name.equals(name))
.map(p -> p.name)
.findFirst()
.orElse("不明な商品");
}
// filter + map + orElseの組み合わせ
public String findExpensiveProduct(Long id, int threshold) {
return Optional.ofNullable(products.get(id))
.filter(p -> p.price >= threshold)
.map(p -> p.name)
.orElse("該当なし");
}
public static void main(String[] args) {
ProductService service = new ProductService();
System.out.println(service.findById(1L).name); // ノートPC
System.out.println(service.findByName("マウス")); // マウス
System.out.println(service.findByName("タブレット")); // 不明な商品
System.out.println(service.findExpensiveProduct(1L, 100000)); // ノートPC
System.out.println(service.findExpensiveProduct(2L, 100000)); // 該当なし
System.out.println(service.findExpensiveProduct(99L, 100000)); // 該当なし
}
}
-
findById:orElseThrowでカスタム例外をスロー -
findByName:Stream APIで名前検索し、orElseでデフォルト値 -
findExpensiveProduct:Optionalのfilter→map→orElseを連鎖させて、条件に合う商品名を取得
参考
- https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
- https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html
- https://spring.io/guides/gs/accessing-data-jpa
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!