JavaのOptionalでnullを安全に扱う方法|NullPointerExceptionを防ぎ、DAOの戻り値をわかりやすくする
こんにちは。ゆうせいです。
今回は、JavaのOptionalを使ってnullを安全に扱う方法を、新人エンジニア向けに解説します。
特に、DAOの戻り値とOptionalの相性が良い理由をしっかり説明します。
Javaを学び始めると、多くの人が一度はNullPointerExceptionに苦しみます。
Exception in thread "main" java.lang.NullPointerException
このエラーを見ると、最初はかなり焦りますよね。
NullPointerExceptionは、ざっくり言うと「中身がないものに対して、メソッドやフィールドを使おうとした」というエラーです。
たとえるなら、空の封筒を開けて「中の書類を読もう」としているような状態です。
封筒の中に書類がないのに読もうとしたら、当然困ります。
Javaでも、nullに対してgetName()やgetEmail()のようなメソッドを呼ぶと、NullPointerExceptionが発生します。
Optionalは、この「値があるかもしれないし、ないかもしれない」という状態を、コード上で分かりやすく表現するための道具です。
OracleのJava APIドキュメントでは、Optionalは「非nullの値を含んでいるかもしれないし、含んでいないかもしれないコンテナオブジェクト」と説明されています。値がある場合はisPresent()がtrueになり、値がない場合はemptyとみなされます。
NullPointerExceptionはなぜ起きるのか
まず、NullPointerExceptionが起きる典型例を見てみましょう。
public class Main {
public static void main(String[] args) {
UserDto user = null;
System.out.println(user.getName());
}
}このコードでは、userにnullが入っています。
その状態でuser.getName()を呼んでいます。
しかし、userは実体のあるUserDtoではありません。
何も入っていないnullです。
そのため、Javaは「nullに対してgetName()は呼べません」と怒ります。
それがNullPointerExceptionです。
| 状態 | 意味 | 結果 |
|---|---|---|
| user = new UserDto() | UserDtoの実体がある | getName()を呼べる |
| user = null | UserDtoの実体がない | getName()でNullPointerException |
nullは、「空っぽの箱」ですらありません。
箱そのものがない状態です。
Optionalは、「箱は必ずある。ただし、中身が入っている場合と、空の場合がある」という考え方に変えてくれます。
Optionalとは何か
Optionalは、値を直接返す代わりに、「値があるかもしれない箱」を返すためのクラスです。
Optional<UserDto>
この書き方は、「UserDtoが入っているかもしれないし、入っていないかもしれない」という意味です。
UserDtoそのものを返す場合、nullかもしれないという危険があります。
Optional<UserDto>を返す場合、「存在しない可能性がありますよ」とメソッドの戻り値の型で伝えられます。
| 戻り値 | 意味 | 危険性 |
|---|---|---|
| UserDto | UserDtoを返す | nullが返る可能性が見えにくい |
| Optional<UserDto> | UserDtoがあるかもしれない | 存在しない場合の処理を意識しやすい |
Optionalは、「この値は存在しない可能性があります」という看板を立てるようなものです。
看板があれば、使う側は慎重になります。
看板がないと、「あると思っていたのにnullだった」という事故が起きます。
DAOの戻り値とOptionalは相性が良い
Optionalが特に力を発揮するのが、DAOの戻り値です。
DAOとは、Data Access Objectの略です。
データベースにアクセスするためのクラスです。
たとえば、ユーザーIDで1件検索するDAOメソッドを考えます。
public UserDto findById(long userId)
このメソッドは、指定されたuserIdのユーザーを探します。
しかし、データベースにそのユーザーが存在しない場合もあります。
ここが大事です。
DAOの1件検索は、「見つかるかもしれないし、見つからないかもしれない」処理です。
つまり、Optionalの考え方と非常に相性が良いのです。
public Optional<UserDto> findById(long userId)
この戻り値なら、メソッドを見ただけで「この検索結果は存在しない可能性がある」と分かります。
新人エンジニアは、まず次の感覚を持ってください。
DAOで1件検索する
↓
見つかるとは限らない
↓
nullを返すと危ない
↓
Optionalで返すと安全に扱いやすいOptionalは、DAOの「1件あるかないか問題」を表現するのに向いています。
nullを返すDAOの悪い例
まず、Optionalを使わない悪い例を見てみましょう。
public UserDto findById(long userId) {
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE user_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
return null;
}
} catch (SQLException e) {
throw new RuntimeException("ユーザー検索に失敗しました。", e);
}
}このコードでは、ユーザーが見つからなかった場合にnullを返しています。
return null;
この書き方は危険です。
呼び出し側がnullチェックを忘れると、すぐにNullPointerExceptionにつながります。
UserDto user = userDao.findById(100); System.out.println(user.getName());
もしuserId=100のユーザーが存在しなければ、userはnullです。
その状態でuser.getName()を呼ぶため、NullPointerExceptionが発生します。
DAOがnullを返す設計では、呼び出し側が毎回nullチェックを覚えていなければいけません。
人間の記憶に頼る設計は、バグのもとです。
Optionalを返すDAOの良い例
次に、Optionalを使ったDAOを見てみましょう。
package com.example.demo.model.dao;
import com.example.demo.model.dto.UserDto;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao extends SuperDao {
public Optional<UserDto> findById(long userId) {
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE user_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return Optional.of(user);
}
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("ユーザー検索に失敗しました。", e);
}
}
}見つかった場合は、Optional.of(user)を返します。
return Optional.of(user);
見つからなかった場合は、Optional.empty()を返します。
return Optional.empty();
nullは返していません。
戻り値は必ずOptionalです。
そのOptionalの中にUserDtoが入っているか、空なのかを表現しています。
| 検索結果 | 返す値 | 意味 |
|---|---|---|
| ユーザーが見つかった | Optional.of(user) | UserDtoが入っている |
| ユーザーが見つからなかった | Optional.empty() | 中身が空 |
この設計にすると、呼び出し側は「見つからない場合」を意識せざるを得ません。
それがOptionalの大きなメリットです。
Optionalの基本メソッド
Optionalでよく使う基本メソッドを整理します。
| メソッド | 意味 | よく使う場面 |
|---|---|---|
| Optional.of(value) | nullではない値をOptionalに入れる | 値が必ずあると分かっているとき |
| Optional.ofNullable(value) | nullかもしれない値をOptionalにする | 外部から来た値を包むとき |
| Optional.empty() | 空のOptionalを作る | 検索結果がなかったとき |
| isPresent() | 値があるか確認する | 条件分岐したいとき |
| isEmpty() | 値がないか確認する | Java 11以降で空判定したいとき |
| orElse(defaultValue) | 値がなければデフォルト値を返す | 代替値を使いたいとき |
| orElseGet(supplier) | 値がなければ処理結果を返す | 代替値の作成が重いとき |
| orElseThrow() | 値がなければ例外を投げる | 見つからない場合をエラーにしたいとき |
| ifPresent(consumer) | 値があるときだけ処理する | 存在する場合だけ表示や処理をしたいとき |
| map(function) | 中身を別の値に変換する | DTOから名前だけ取り出したいとき |
最初から全部を完璧に覚える必要はありません。
DAOの戻り値で使うなら、まずOptional.of、Optional.empty、orElseThrow、ifPresentを覚えれば十分です。
Optional.ofとOptional.ofNullableの違い
Optionalを作るとき、ofとofNullableの違いに注意してください。
String name = "山田太郎"; Optional<String> optionalName = Optional.of(name);
Optional.ofは、値がnullではないと分かっている場合に使います。
nullを渡すとNullPointerExceptionになります。
String name = null; Optional<String> optionalName = Optional.of(name);
このコードは危険です。
nameがnullなので、Optional.of(name)の時点でNullPointerExceptionが発生します。
nullかもしれない値をOptionalにしたい場合は、ofNullableを使います。
String name = null; Optional<String> optionalName = Optional.ofNullable(name);
この場合、optionalNameは空のOptionalになります。
| メソッド | nullを渡した場合 | 使いどころ |
|---|---|---|
| Optional.of(value) | NullPointerException | 絶対にnullではない値 |
| Optional.ofNullable(value) | Optional.empty() | nullかもしれない値 |
DAOでResultSetからDTOを作った直後のuserは、new UserDto()で作っているのでnullではありません。
そのためOptional.of(user)で問題ありません。
一方、どこかから受け取った値がnullかもしれない場合は、Optional.ofNullableを使います。
ServiceでOptionalを扱う
DAOがOptionalを返す場合、Serviceでは次のように扱えます。
package com.example.demo.service;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.dao.UserDao;
import com.example.demo.model.dto.UserDto;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public UserDto findById(long userId) {
return userDao.findById(userId)
.orElseThrow(() ->
new UserNotFoundException("ユーザーが見つかりません。userId=" + userId)
);
}
}orElseThrowは、「値があれば取り出す。値がなければ例外を投げる」というメソッドです。
ユーザー詳細画面のように、指定されたIDのユーザーが存在しないなら404エラーにしたい場合に向いています。
return userDao.findById(userId)
.orElseThrow(() ->
new UserNotFoundException("ユーザーが見つかりません。userId=" + userId)
);
このコードは、次のif文と似た意味です。
Optional<UserDto> optionalUser = userDao.findById(userId);
if (optionalUser.isPresent()) {
return optionalUser.get();
}
throw new UserNotFoundException("ユーザーが見つかりません。userId=" + userId);
ただし、orElseThrowを使うほうが、処理の意図が短く書けます。
「なければ例外」という業務ルールが読み取りやすくなります。
独自例外を作る
ユーザーが見つからない場合の独自例外を作っておきます。
package com.example.demo.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}独自例外を使うと、何が起きたのかが分かりやすくなります。
単にRuntimeExceptionを投げるより、UserNotFoundExceptionのほうが意味が明確です。
たとえるなら、ただ「エラーです」と言うより、「ユーザーが見つかりません」と言うほうが相手に伝わりますよね。
Controllerで使う
Serviceを呼び出すControllerの例です。
package com.example.demo.controller;
import com.example.demo.model.dto.UserDto;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/detail")
public String detail(@RequestParam long userId, Model model) {
UserDto user = userService.findById(userId);
model.addAttribute("user", user);
return "users/detail";
}
}Controllerでは、Optionalを直接扱っていません。
Serviceが「見つからなければ例外」というルールを処理しています。
このようにすると、Controllerは画面表示に集中できます。
| 層 | 役割 |
|---|---|
| DAO | DB検索結果をOptionalで返す |
| Service | Optionalを業務ルールに変換する |
| Controller | 画面表示に必要なデータをModelへ入れる |
DAOは「見つかったか、見つからなかったか」を返す係です。
Serviceは「見つからなかった場合にどう扱うか」を決める係です。
Controllerは「画面へ渡す」係です。
@ControllerAdviceと組み合わせる
UserNotFoundExceptionを投げるなら、@ControllerAdviceで共通処理にできます。
package com.example.demo.exception;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public String handleUserNotFoundException(
UserNotFoundException e,
Model model) {
model.addAttribute("errorMessage", e.getMessage());
return "error/404";
}
@ExceptionHandler(Exception.class)
public String handleException(Model model) {
model.addAttribute("errorMessage", "予期しないエラーが発生しました。");
return "error/500";
}
}この設計にすると、ユーザーが見つからない場合はerror/404.htmlを表示できます。
DAOでnullを返してControllerでNullPointerExceptionになるより、はるかに分かりやすいです。
DAO
↓
Optional.empty()
↓
Service
↓
orElseThrow
↓
UserNotFoundException
↓
@ControllerAdvice
↓
404画面この流れはとてもきれいです。
「見つからない」という正常に想定できるケースを、NullPointerExceptionではなく、意味のある例外として扱えます。
悪い例:Optional.getをいきなり使う
Optionalを使っていても、使い方を間違えると危険です。
特に避けたいのが、いきなりget()する書き方です。
Optional<UserDto> optionalUser = userDao.findById(userId); UserDto user = optionalUser.get();
このコードは、Optionalの中身が空の場合にNoSuchElementExceptionになります。
nullではなくなりましたが、「値がないのに無理やり取り出す」という問題は残っています。
空の冷蔵庫を開けて、「牛乳を取り出す!」と言っているようなものです。
冷蔵庫が空なら、牛乳は取り出せません。
Optionalを使うなら、値がない場合を考えてください。
良い例です。
UserDto user = userDao.findById(userId)
.orElseThrow(() ->
new UserNotFoundException("ユーザーが見つかりません。userId=" + userId)
);
または、存在する場合だけ処理します。
userDao.findById(userId)
.ifPresent(user -> {
System.out.println(user.getName());
});
Optional.getを使う前には、「空だったらどうするのか?」を必ず考えてください。
isPresentを使う書き方
isPresentを使うと、値があるかどうかを確認できます。
Optional<UserDto> optionalUser = userDao.findById(userId);
if (optionalUser.isPresent()) {
UserDto user = optionalUser.get();
System.out.println(user.getName());
} else {
System.out.println("ユーザーが見つかりません。");
}この書き方は分かりやすいです。
新人エンジニアがOptionalを理解する最初の段階では、この書き方から入ってもよいです。
ただし、毎回isPresentとgetを書くと、少し冗長になります。
慣れてきたら、orElseThrow、ifPresent、mapなどを使って、より意図が伝わる書き方にしていきましょう。
| 書き方 | 特徴 |
|---|---|
| isPresent + get | 初心者には分かりやすい |
| orElseThrow | なければ例外、という意図が明確 |
| ifPresent | ある場合だけ処理できる |
| map | 中身を変換できる |
orElseでデフォルト値を返す
値がない場合に代わりの値を使いたい場合は、orElseを使えます。
String userName = userDao.findById(userId)
.map(user -> user.getName())
.orElse("ゲストユーザー");このコードは、ユーザーが見つかった場合はその名前を使います。
見つからなかった場合は、ゲストユーザーを使います。
mapは、Optionalの中身を別の値に変換するメソッドです。
map(user -> user.getName())
UserDtoからnameだけを取り出しています。
このように、Optionalを使うと「値があったら変換し、なければ代わりを使う」という流れを表現できます。
orElseとorElseGetの違い
orElseとorElseGetは似ていますが、違いがあります。
String name = optionalName.orElse(createDefaultName());
String name = optionalName.orElseGet(() -> createDefaultName());
orElseでは、Optionalに値があってもcreateDefaultName()が先に実行されることがあります。
orElseGetでは、Optionalが空のときだけcreateDefaultName()が実行されます。
デフォルト値を作る処理が軽いならorElseでも問題ありません。
DBアクセスや重い計算があるならorElseGetを使うほうが向いています。
| メソッド | 特徴 | 使いどころ |
|---|---|---|
| orElse | デフォルト値を直接渡す | 固定文字列や軽い値 |
| orElseGet | 空のときだけ処理を実行する | 重い処理、必要なときだけ作りたい値 |
たとえるなら、orElseは「予備の弁当を先に作っておく」イメージです。
orElseGetは「必要になったときだけ弁当を作る」イメージです。
findByEmailでもOptionalは相性が良い
DAOでは、ID検索だけでなく、メールアドレス検索でもOptionalが向いています。
public Optional<UserDto> findByEmail(String email) {
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE email = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, email);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return Optional.of(user);
}
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("メールアドレスによるユーザー検索に失敗しました。", e);
}
}ログイン処理では、メールアドレスでユーザーを検索することがあります。
そのメールアドレスのユーザーが存在しない場合もあります。
そのため、findByEmailの戻り値もOptional<UserDto>にすると自然です。
ログイン処理でOptionalを使う例
ログイン処理では、ユーザーが見つからない場合と、パスワードが違う場合があります。
Optionalを使うと、ユーザーが見つからないケースを安全に扱えます。
public Optional<UserDto> login(String email, String rawPassword) {
Optional<UserDto> optionalUser = userDao.findByEmail(email);
if (optionalUser.isEmpty()) {
return Optional.empty();
}
UserDto user = optionalUser.get();
boolean matched =
passwordEncoder.matches(rawPassword, user.getPasswordHash());
if (!matched) {
return Optional.empty();
}
return Optional.of(user);
}このServiceは、ログイン成功ならOptional.of(user)を返します。
失敗ならOptional.empty()を返します。
Controller側では、次のように扱えます。
@PostMapping("/login")
public String login(
@RequestParam String email,
@RequestParam String password,
HttpSession session,
Model model) {
Optional<UserDto> optionalUser =
userService.login(email, password);
if (optionalUser.isEmpty()) {
model.addAttribute("errorMessage",
"メールアドレスまたはパスワードが正しくありません。");
return "login";
}
session.setAttribute("loginUser", optionalUser.get());
return "redirect:/mypage";
}このように、ログイン成功と失敗をOptionalで表現できます。
ただし、ここでもget()を使う前にisEmptyでチェックしています。
チェックなしのget()は避けましょう。
Listを返すDAOではOptionalにしない
OptionalはDAOの1件検索と相性が良いです。
しかし、一覧検索では基本的にOptional<List<UserDto>>にはしません。
一覧検索では、見つからない場合は空のListを返すほうが自然です。
悪い例です。
public Optional<List<UserDto>> findAll() {
...
}
良い例です。
public List<UserDto> findAll() {
...
}
データが0件なら、空のListを返します。
return new ArrayList<>();
| 検索の種類 | おすすめの戻り値 | 理由 |
|---|---|---|
| 1件検索 | Optional<UserDto> | あるかないかを表現しやすい |
| 一覧検索 | List<UserDto> | 0件なら空リストで表現できる |
| 件数取得 | intやlong | 0件なら0で表現できる |
Optionalは「値が1つあるか、ないか」を表すのが得意です。
一覧は「0件以上の集合」なので、Listで十分です。
Optionalをフィールドに使うべきか
新人エンジニアが迷いやすいポイントとして、DTOのフィールドにOptionalを使うべきかという話があります。
基本的には、DTOのフィールドにOptionalを使うのは避けたほうが分かりやすいです。
避けたい例です。
public class UserDto {
private Optional<String> name;
private Optional<String> email;
}
通常は、次のように書きます。
public class UserDto {
private String name;
private String email;
}
Optionalは主にメソッドの戻り値として使うと考えてください。
特にDAOのfindByIdやfindByEmailのような「1件見つかるかどうか」を表す戻り値に向いています。
| 使う場所 | おすすめ度 | 理由 |
|---|---|---|
| DAOの1件検索戻り値 | 高い | 存在しない可能性を型で表現できる |
| Serviceの検索戻り値 | 場合による | 呼び出し側に判断させたい場合は有効 |
| DTOのフィールド | 低い | 扱いが複雑になりやすい |
| メソッド引数 | 低い | 呼び出し側が面倒になりやすい |
Optionalは便利ですが、どこにでも使えばよいわけではありません。
包丁が便利でも、すべての作業に包丁を使うわけではありませんよね。
Optionalも、向いている場所で使うことが大切です。
Optionalをnullにしてはいけない
Optionalを使うときの大きなルールがあります。
Optionalそのものをnullにしてはいけません。
悪い例です。
Optional<UserDto> optionalUser = null;
この書き方をすると、Optionalを使う意味がありません。
optionalUser.isPresent()を呼んだ瞬間に、NullPointerExceptionが発生します。
Optionalは「箱は必ずある。中身があるか空かで表現する」ための道具です。
空を表したいなら、nullではなくOptional.empty()を使います。
Optional<UserDto> optionalUser = Optional.empty();
このルールは必ず守ってください。
OptionalでNullPointerExceptionは完全になくなるのか
Optionalを使えば、NullPointerExceptionが完全にゼロになるわけではありません。
Optional.of(null)を使えばNullPointerExceptionになります。
Optional変数そのものをnullにすればNullPointerExceptionになります。
get()を乱用すれば、NoSuchElementExceptionになります。
つまり、Optionalは魔法ではありません。
ただし、nullの危険を型として見えるようにし、「値がない場合」を考えやすくしてくれます。
| 危険な書き方 | 問題 | 改善 |
|---|---|---|
| return null; | 呼び出し側が気づきにくい | return Optional.empty(); |
| Optional.of(null) | NullPointerException | Optional.ofNullable(value) |
| optional.get() | 空なら例外 | orElseThrow、ifPresent、orElse |
| Optional変数にnull | Optionalの意味が消える | Optional.empty() |
Optionalを使う目的は、nullを完全に消すことではありません。
「nullかもしれない」というあいまいさを、明示的な設計に変えることです。
DAO、Service、Controllerの完成イメージ
最後に、Optionalを使った基本構成をまとめます。
UserDtoです。
package com.example.demo.model.dto;
public class UserDto {
private long userId;
private String name;
private String email;
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}UserDaoです。
package com.example.demo.model.dao;
import com.example.demo.model.dto.UserDto;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao extends SuperDao {
public Optional<UserDto> findById(long userId) {
String sql =
"SELECT user_id, name, email "
+ "FROM users "
+ "WHERE user_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UserDto user = new UserDto();
user.setUserId(rs.getLong("user_id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return Optional.of(user);
}
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("ユーザー検索に失敗しました。", e);
}
}
}UserServiceです。
package com.example.demo.service;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.model.dao.UserDao;
import com.example.demo.model.dto.UserDto;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public UserDto findById(long userId) {
return userDao.findById(userId)
.orElseThrow(() ->
new UserNotFoundException("ユーザーが見つかりません。userId=" + userId)
);
}
}UserControllerです。
package com.example.demo.controller;
import com.example.demo.model.dto.UserDto;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/detail")
public String detail(@RequestParam long userId, Model model) {
UserDto user = userService.findById(userId);
model.addAttribute("user", user);
return "users/detail";
}
}この設計では、DAOがnullを返しません。
ServiceがOptionalを受け取り、見つからなければ意味のある例外を投げます。
Controllerは、取得できたUserDtoを画面に渡すだけです。
役割がきれいに分かれています。
新人エンジニアが最初に覚えるべきOptionalのルール
| ルール | 理由 |
|---|---|
| DAOの1件検索はOptionalを検討する | 見つからない可能性を表現できる |
| nullを返さずOptional.empty()を返す | NullPointerExceptionを避けやすい |
| 値があるならOptional.of(value) | 存在する値をOptionalに包める |
| nullかもしれないならOptional.ofNullable(value) | nullを空のOptionalに変換できる |
| get()をいきなり使わない | 空の場合に例外になる |
| なければエラーならorElseThrowを使う | 業務ルールを表現しやすい |
| 一覧検索はListを返す | 0件は空リストで表現できる |
| Optionalそのものをnullにしない | Optionalを使う意味がなくなる |
このルールを守るだけでも、NullPointerExceptionはかなり減らせます。
まとめ
Optionalは、Javaでnullを安全に扱うための重要なクラスです。
特にDAOの1件検索の戻り値と相性が良く、「見つかるかもしれないし、見つからないかもしれない」という状態を型で表現できます。
| 用語 | 意味 |
|---|---|
| null | 参照先がない状態 |
| NullPointerException | nullに対してメソッドなどを使ったときに起きる例外 |
| Optional | 値があるかもしれないし、空かもしれない箱 |
| Optional.of | nullではない値をOptionalに入れる |
| Optional.empty | 空のOptionalを作る |
| orElseThrow | 値がなければ例外を投げる |
| DAO | DBアクセスを担当するクラス |
一言でまとめるなら、Optionalは「nullかもしれない戻り値を、安全に扱うための箱」です。
DAOでは、特に次の形を覚えてください。
public Optional<UserDto> findById(long userId) {
...
}
見つかったらOptional.of(user)。
見つからなかったらOptional.empty()。
ServiceではorElseThrowで、見つからない場合の業務ルールへ変換します。
UserDto user = userDao.findById(userId)
.orElseThrow(() ->
new UserNotFoundException("ユーザーが見つかりません。")
);
新人エンジニアは、まず「DAOの1件検索でnullを返さない」ことを目標にしてください。
nullを返す代わりにOptional.empty()を返す。
呼び出し側では、orElseThrow、orElse、ifPresentを使って、値がない場合を明示的に扱う。
この考え方が身につくと、NullPointerExceptionに振り回される時間がかなり減ります!
今後の学習では、Optional.of、Optional.ofNullable、Optional.empty、orElseThrow、orElseGet、ifPresent、map、DAOのfindById設計、@ControllerAdviceによる404処理を順番に学ぶとよいです。まずは既存のDAOでnullを返している1件検索を探し、Optional<Dto>を返す形に書き換えてみましょう。
セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。
投稿者プロフィール

- 代表取締役
-
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。
学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。
最新の投稿
新人エンジニア研修講師2026年6月18日JavaのOptionalでnullを安全に扱う方法|NullPointerExceptionを防ぎ、DAOの戻り値をわかりやすくする
新人エンジニア研修講師2026年6月18日ローカルSMTPを使って問い合わせ完了メールを送る方法|新人エンジニア研修向けにSpring BootとJavaMailSenderを解説
新人エンジニア研修講師2026年6月18日DevToolsでHTTP通信・エラー・DOMを確認する方法|新人エンジニア向けにブラウザ開発者ツールを解説
新人エンジニア研修講師2026年6月18日Spring Bootの@RestControllerとは?Webアプリケーションを学んだ新人エンジニア向けにAPIをやさしく解説
