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 = nullUserDtoの実体がないgetName()でNullPointerException

nullは、「空っぽの箱」ですらありません。

箱そのものがない状態です。

Optionalは、「箱は必ずある。ただし、中身が入っている場合と、空の場合がある」という考え方に変えてくれます。

Optionalとは何か

Optionalは、値を直接返す代わりに、「値があるかもしれない箱」を返すためのクラスです。

Optional<UserDto>

この書き方は、「UserDtoが入っているかもしれないし、入っていないかもしれない」という意味です。

UserDtoそのものを返す場合、nullかもしれないという危険があります。

Optional<UserDto>を返す場合、「存在しない可能性がありますよ」とメソッドの戻り値の型で伝えられます。

戻り値意味危険性
UserDtoUserDtoを返す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は画面表示に集中できます。

役割
DAODB検索結果をOptionalで返す
ServiceOptionalを業務ルールに変換する
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やlong0件なら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)NullPointerExceptionOptional.ofNullable(value)
optional.get()空なら例外orElseThrow、ifPresent、orElse
Optional変数にnullOptionalの意味が消える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参照先がない状態
NullPointerExceptionnullに対してメソッドなどを使ったときに起きる例外
Optional値があるかもしれないし、空かもしれない箱
Optional.ofnullではない値をOptionalに入れる
Optional.empty空のOptionalを作る
orElseThrow値がなければ例外を投げる
DAODBアクセスを担当するクラス

一言でまとめるなら、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年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。