【Java】nullとOptionalの違い・使い方を初心者向けに解説

Javaを勉強していると、null という値がよく出てきます。

null は「値が存在しない」ことを表す特別な値です。

例えば、以下のように String 型の変数に null を入れることができます。

String name = null;

ただし、null のままメソッドを呼び出すと、NullPointerException が発生します。

String name = null;

System.out.println(name.length());

このコードでは、name が null なのに length メソッドを呼び出しているため、エラーになります。

Javaでは、この null の扱いを誤ると、実行時エラーの原因になります。

また、Java 8以降では Optional という仕組みも登場しました。

Optional は、値が存在するかもしれないし、存在しないかもしれない、という状態を表すための型です。

Javaを学び始めたばかりの頃は、以下のような点で迷いやすいです。

  • null とは何か
  • NullPointerException はなぜ発生するのか
  • int と Integer の null の扱いは何が違うのか
  • 画面入力の未入力は null で扱ってよいのか
  • Optional はどのような場面で使うのか
  • Optional を引数やフィールドに使ってよいのか

この記事では、Javaにおける null と Optional について、基本的な考え方から実務での使い分けまでまとめます。

スポンサーリンク

※このページにはプロモーションが含まれています。当サイトは各種アフィリエイトプログラムから一定の収益を得ています。

nullとは何か

null とは、参照型の変数に値が入っていない状態を表す特別な値です。

例えば、以下のようなコードです。

String name = null;
Integer age = null;
User user = null;

この場合、変数自体は存在していますが、参照先のオブジェクトが存在していません。

そのため、null の状態でメソッドを呼び出すと、NullPointerException が発生します。

String name = null;

System.out.println(name.length());

name は String 型ですが、実際には String オブジェクトを参照していません。

そのため、length メソッドを呼び出す対象が存在せず、NullPointerException になります。

基本型はnullにならない

Javaには、大きく分けて基本型と参照型があります。

  • 基本型・・・int、long、double、boolean など
  • 参照型・・・String、Integer、Long、List、独自クラスなど

基本型は null を持てません。

int count = 0;
boolean active = false;
double price = 0.0;

以下のように、int に null を入れることはできません。

int count = null;

一方で、Integer などのラッパークラスは参照型なので null を持てます。

Integer count = null;
Boolean active = null;
Double price = null;

つまり、未入力や値なしを表現したい場合は、int ではなく Integer を使う必要があります。

intとIntegerの使い分け

int と Integer の大きな違いは、null を扱えるかどうかです。

  • int・・・nullにできない
  • Integer・・・nullにできる

例えば、画面から数値を受け取るケースを考えます。

年齢が未入力になる可能性がある場合、int で受け取ると未入力を表現できません。

private int age;

int は null にできないため、未入力なのか、0 が入力されたのかを区別しにくくなります。

未入力と 0 を区別したい場合は、Integer を使います。

private Integer age;

この場合、未入力であれば null として扱えます。

if (age == null) {
    System.out.println("年齢は未入力です");
}

画面入力の未入力はnullで扱ってよいのか

画面から渡される数値項目で、未入力の可能性がある場合は、Integer で受け取るのが自然です。

例えば、Spring Boot のフォームクラスやリクエストDTOでは、以下のように定義します。

public class UserForm {
    private Integer age;
    private Integer quantity;

    public Integer getAge() {
        return age;
    }

    public Integer getQuantity() {
        return quantity;
    }
}

未入力の場合、age や quantity は null になる可能性があります。

そのため、計算や比較をする前には null チェックが必要です。

Integer quantity = form.getQuantity();

if (quantity != null) {
    int total = quantity * 100;
    System.out.println(total);
}

もし quantity が null のまま計算すると、NullPointerException が発生する可能性があります。

Integer quantity = null;

int total = quantity * 100;

Integer は参照型ですが、計算時には int に変換されます。

このとき、Integer が null だと変換できないため、NullPointerException になります。

未入力の場合の扱いは仕様で決める

画面入力の未入力を null で受け取ること自体は問題ありません。

ただし、その null をどのように扱うかは、業務仕様として決める必要があります。

  • 必須項目ならエラーにする
  • 任意項目なら処理しない
  • 未入力を 0 として扱う
  • 未入力をデフォルト値に置き換える

例えば、数量が必須項目なら、null のまま処理を進めるべきではありません。

if (form.getQuantity() == null) {
    throw new IllegalArgumentException("数量は必須です");
}

一方で、割引率のような任意項目であれば、未入力の場合は処理しない、という設計もあります。

Integer discountRate = form.getDiscountRate();

if (discountRate != null) {
    price = price - price * discountRate / 100;
}

また、未入力を 0 として扱ってよい場合は、null を 0 に変換してから処理する方法もあります。

int discountRate = form.getDiscountRate() == null ? 0 : form.getDiscountRate();

ただし、何でも 0 にすればよいわけではありません。

例えば、年齢が未入力の場合に 0 歳として扱うと、業務的に不自然なデータになる可能性があります。

未入力と 0 を同じ意味として扱ってよいかは、項目ごとに判断する必要があります。

null安全の基本的な考え方

null安全とは、NullPointerException が発生しにくいように設計・実装する考え方です。

単に null チェックを増やすことだけが null安全ではありません。

重要なのは、null が入る可能性のある場所を限定し、どこで null を処理するかを明確にすることです。

  • 画面入力では null の可能性を受け入れる
  • 入力チェックで必須項目の null を弾く
  • 業務処理に入る前に、必要な値が揃っている状態にする
  • 処理の奥深くまで null を引き回さない

例えば、以下のように処理の最初で必須チェックを行うと、その後の処理がシンプルになります。

public void register(UserForm form) {
    if (form.getName() == null) {
        throw new IllegalArgumentException("名前は必須です");
    }

    if (form.getAge() == null) {
        throw new IllegalArgumentException("年齢は必須です");
    }

    createUser(form.getName(), form.getAge());
}

このように、入口でチェックしておくことで、後続処理では値が存在する前提で扱いやすくなります。

nullを返さない設計も重要

null安全を考えるうえでは、できるだけ null を返さない設計も重要です。

例えば、一覧取得メソッドでデータが0件の場合、null を返すよりも空のリストを返す方が安全です。

あまりよくない例です。

public List<String> getNames() {
    return null;
}

この場合、呼び出し側で毎回 null チェックが必要になります。

List<String> names = getNames();

if (names != null) {
    System.out.println(names.size());
}

よい例です。

public List<String> getNames() {
    return new ArrayList<>();
}

空のリストを返せば、呼び出し側はそのまま size メソッドを呼び出せます。

List<String> names = getNames();

System.out.println(names.size());

この場合、件数は 0 になりますが、NullPointerException は発生しません。

文字列比較では定数を左側に置く

Javaで文字列を比較するときにも、null に注意が必要です。

例えば、以下のコードは name が null の場合に NullPointerException になります。

if (name.equals("admin")) {
    System.out.println("管理者です");
}

安全に書くなら、定数を左側に置きます。

if ("admin".equals(name)) {
    System.out.println("管理者です");
}

この書き方であれば、name が null でも NullPointerException は発生しません。

Optionalとは何か

Optional は、値が存在するかもしれないし、存在しないかもしれない、という状態を表すための型です。

通常、値がないことを表すために null を使うことがあります。

User user = findUserById(userId);

この findUserById メソッドが、ユーザーが見つからない場合に null を返す設計だったとします。

呼び出し側が null の可能性を忘れてしまうと、NullPointerException の原因になります。

User user = findUserById(userId);

System.out.println(user.getName());

Optional を使うと、値がない可能性を戻り値の型として表現できます。

public Optional<User> findUserById(Long userId) {
    User user = userRepository.findById(userId);
    return Optional.ofNullable(user);
}

このメソッドの戻り値を見ると、呼び出し側は「ユーザーが存在しない可能性がある」と分かります。

Optionalの作り方

Optional を作る方法はいくつかあります。

値が必ず null ではない場合は、Optional.of を使います。

Optional<String> name = Optional.of("田中");

ただし、Optional.of に null を渡すと NullPointerException になります。

Optional<String> name = Optional.of(null);

null の可能性がある値を Optional にする場合は、Optional.ofNullable を使います。

String inputName = null;

Optional<String> name = Optional.ofNullable(inputName);

inputName が null の場合、Optional.empty になります。

明示的に空の Optional を作る場合は、Optional.empty を使います。

Optional<String> name = Optional.empty();

Optionalの基本的な使い方

Optional の値を扱う方法をいくつか見ていきます。

値がある場合だけ処理する

値がある場合だけ処理したい場合は、ifPresent を使います。

Optional<String> name = Optional.ofNullable(getName());

name.ifPresent(value -> {
    System.out.println(value.length());
});

name に値があれば処理されます。

値がなければ、何も実行されません。

値がない場合にデフォルト値を使う

値がない場合に別の値を使いたい場合は、orElse を使います。

String name = Optional.ofNullable(getName())
        .orElse("未設定");

getName の結果が null の場合、name には「未設定」が入ります。

値がない場合に例外を投げる

値がない場合に処理を続けてはいけない場合は、orElseThrow を使います。

User user = userRepository.findUserById(userId)
        .orElseThrow(() -> new IllegalArgumentException("ユーザーが存在しません"));

このように書くと、ユーザーが見つからない場合は例外になります。

一方で、ユーザーが存在する場合は、Optional ではなく User として後続処理に進めます。

値を変換する

Optional の中の値を変換したい場合は、map を使います。

Optional<Integer> length = Optional.ofNullable(getName())
        .map(String::length);

getName の結果が null でなければ、文字数に変換されます。

getName の結果が null であれば、Optional.empty になります。

条件に合う値だけ残す

Optional の中の値が条件に合う場合だけ残したい場合は、filter を使います。

Optional<Integer> adultAge = Optional.ofNullable(getAge())
        .filter(age -> age >= 20);

getAge の結果が 20 以上であれば、値が残ります。

null または 20 未満であれば、Optional.empty になります。

Optionalは主に戻り値で使う

Optional は、主にメソッドの戻り値として使うのが一般的です。

特に、検索メソッドのように「見つからない可能性がある」処理では Optional が向いています。

例えば、ユーザーIDからユーザーを検索するメソッドです。

public Optional<User> findUserById(Long userId) {
    User user = jdbcTemplate.queryForObject(...);
    return Optional.ofNullable(user);
}

このように書くことで、呼び出し側に「ユーザーが見つからない可能性がある」と伝えられます。

呼び出し側では、存在しない場合の処理を明確に書きます。

Optional<User> userOpt = userRepository.findUserById(userId);

if (userOpt.isEmpty()) {
    return;
}

User user = userOpt.get();

sendMail(user);
updateHistory(user);

この例では、ユーザーが存在しない場合は return しています。

その後は User が存在する前提で処理できます。

Optionalを引数に渡し続けない

Optional は戻り値としては便利ですが、引数として使うと処理が分かりづらくなることがあります。

例えば、以下のように Optional を複数のメソッドに渡すコードを考えます。

public void process(Long userId) {
    Optional<User> userOpt = userRepository.findUserById(userId);

    validateUser(userOpt);
    sendMail(userOpt);
    updateHistory(userOpt);
}

この書き方だと、各メソッドの中で毎回 Optional を確認する必要があります。

private void sendMail(Optional<User> userOpt) {
    userOpt.ifPresent(user -> {
        System.out.println(user.getEmail());
    });
}

このようなコードは、一見安全そうに見えます。

しかし、sendMail メソッドの責務が分かりにくくなります。

  • メール送信だけをするメソッドなのか
  • ユーザーが存在しない場合の判定もするメソッドなのか
  • ユーザーが存在しない場合は何もしない仕様なのか

このように、Optional を引数としてあちこちに渡すと、処理の責務が曖昧になりやすいです。

Optionalは境界で解決する

おすすめは、Optional を受け取った場所で、できるだけ早めに扱いを決めることです。

例えば、ユーザーが存在しない場合にエラーにするなら、orElseThrow で User に戻します。

public void process(Long userId) {
    User user = userRepository.findUserById(userId)
            .orElseThrow(() -> new IllegalArgumentException("ユーザーが存在しません"));

    validateUser(user);
    sendMail(user);
    updateHistory(user);
}

この形であれば、validateUser、sendMail、updateHistory は、User が存在する前提で実装できます。

private void sendMail(User user) {
    System.out.println(user.getEmail());
}

存在しない場合は何もしない仕様なら、ifPresent を使う方法もあります。

public void process(Long userId) {
    userRepository.findUserById(userId)
            .ifPresent(user -> {
                validateUser(user);
                sendMail(user);
                updateHistory(user);
            });
}

この場合も、各メソッドには Optional ではなく User を渡しています。

Optional は「値があるかないかを判断する境界」で使い、その後の処理では通常の型に戻すと分かりやすくなります。

Optionalをフィールドに使うのは避ける

Optional は、基本的にはフィールドには使わない方がよいです。

例えば、以下のようなフォームクラスはあまりおすすめしません。

public class UserForm {
    private Optional<Integer> age;
}

画面入力DTOやEntityでは、通常の型を使うことが多いです。

public class UserForm {
    private Integer age;
}

そして、処理の中で必要に応じて Optional に包みます。

Optional.ofNullable(form.getAge())
        .ifPresent(age -> {
            System.out.println(age);
        });

Optional は「フィールドとして保持するもの」ではなく、「値がない可能性を戻り値として表現するもの」と考えると分かりやすいです。

Optional.getをいきなり使わない

Optional を使うときに注意したいのが、get メソッドです。

以下のコードは危険です。

Optional<String> name = Optional.empty();

String value = name.get();

Optional が空の場合、get を呼び出すと NoSuchElementException が発生します。

get を使う場合は、値が存在することを事前に確認する必要があります。

if (name.isPresent()) {
    String value = name.get();
    System.out.println(value);
}

ただし、isPresent と get の組み合わせばかり使うと、通常の null チェックとあまり変わらなくなります。

できれば、ifPresent、orElse、orElseThrow、map などを使う方が Optional らしい書き方です。

Optionalとnullチェックの使い分け

Optional を使えば、すべての null チェックが不要になるわけではありません。

画面入力、外部API、DBの値など、null が入る可能性がある場所では、null チェックが必要になることもあります。

一方で、検索メソッドの戻り値のように「値がない可能性」を呼び出し側に明示したい場合は Optional が有効です。

  • 画面入力DTOの項目・・・String、Integer などで持つ
  • 必須項目・・・バリデーションや null チェックで弾く
  • 任意項目・・・null の場合の扱いを決める
  • 検索メソッドの戻り値・・・Optional を検討する
  • 後続メソッドの引数・・・Optional ではなく中身の型を渡す

つまり、Optional は null チェックの完全な代わりではありません。

値が存在しない可能性を、型として分かりやすく表現するための道具です。

実務でのおすすめ方針

実務では、次のように考えると分かりやすいです。

  • 未入力がありえる画面項目は Integer や String で受け取る
  • 必須項目は入力チェックで null を弾く
  • 任意項目は null の場合の仕様を明確にする
  • 一覧取得では null ではなく空リストを返す
  • 単一検索で見つからない可能性がある場合は Optional を戻り値にする
  • Optional を受け取ったら、呼び出し元で早めに処理方針を決める
  • Optional を引数やフィールドとして広げすぎない

例えば、単一検索では Optional を使います。

public Optional<User> findUserById(Long userId) {
    User user = userRepository.findById(userId);
    return Optional.ofNullable(user);
}

呼び出し元では、存在しない場合の扱いを決めます。

User user = userRepository.findUserById(userId)
        .orElseThrow(() -> new IllegalArgumentException("ユーザーが存在しません"));

sendMail(user);

このように、Optional は戻り値で使い、後続処理では通常の型に戻すと、コードが読みやすくなります。

まとめ

Javaの null と Optional についてまとめると、以下のようになります。

  • null は値が存在しないことを表す
  • null のままメソッドを呼ぶと NullPointerException になる
  • 基本型は null にできない
  • Integer や String などの参照型は null になる可能性がある
  • 画面入力の未入力を扱う場合は Integer を使うことが多い
  • null の場合にエラーにするのか、処理しないのか、デフォルト値にするのかを仕様で決める
  • Optional は値がない可能性を表現するための型
  • Optional は主に検索メソッドなどの戻り値に使う
  • Optional を引数やフィールドとして使いすぎると分かりづらくなる
  • Optional を受け取ったら、呼び出し元で早めに処理方針を決める

null安全で大切なのは、すべての場所で機械的に null チェックを増やすことではありません。

どこで null を許可するのか、どこで null をチェックするのか、どこから先は値が存在する前提にするのかを明確にすることです。

Optional も同じで、何でも Optional にすれば安全になるわけではありません。

検索メソッドの戻り値のように、値が存在しない可能性を明示したい場面で使うと効果的です。

まずは、画面入力では null の可能性を考慮し、検索メソッドでは Optional を戻り値に使い、後続処理では通常の型に戻して扱う、という方針を覚えておくと実務で迷いにくくなります。

スポンサーリンク

Java