JavaのBooleanをif条件で使うとNullPointerExceptionになる理由を解説する記事のアイキャッチ。

実装・エラー解決

JavaのBooleanでNullPointerExceptionになる理由|自動アンボクシングの注意

AIによる要約

Booleanはtrue・falseに加えてnullを持てます。if(flag)やboolean変数への代入ではBooleanからbooleanへの自動アンボクシングが行われ、flagがnullだとNullPointerExceptionになります。trueだけを判定するならBoolean.TRUE.equals(flag)、必ず二値なら最初からbooleanを使います。nullを『未設定』『未回答』『対象外』として使うなら、三値の意味を明示し、既定値で潰してよいか確認します。

新人SE
新人SE
if (enabled)の行でNullPointerExceptionになりました。enabledはBooleanです。
ポンコツSE
ポンコツSE
Booleanのnullをbooleanへ変換しようとして失敗しています。自動アンボクシングが原因です。

booleanはtrueかfalseだけですが、ラッパークラスのBooleanはnullも持てます。DBのNULL、JSONの項目省略、Map#get、未初期化のDTOなどからnullが入り、if条件へ渡した瞬間に例外になることがあります。

対策としてBoolean.TRUE.equalsを覚えるだけでなく、なぜBooleanを使っているのか確認してください。nullに業務上の意味がないならbooleanへ寄せ、意味があるなら三値を仕様として扱います。

この記事のポイント

  • Booleanはnullを持てるがbooleanは持てない
  • if(flag)では自動アンボクシングが起きる
  • nullをtrue以外として扱うならBoolean.TRUE.equalsを使える
  • 二値で十分ならbooleanを選ぶ
  • 三値のnullをfalseへ潰してよいか業務仕様を確認する

実行サンプル

同意が未回答・拒否・許可の場合の登録結果と処理回数を比較します。 JDK 17以上とMavenを使います。ZIPを展開し、mvn clean testを実行してください。必要なソース、テスト、実行設定を収録しています。

if条件で自動アンボクシングが起きる

ifはboolean値を必要とします。Booleanを渡すと、Javaが内部でflag.booleanValue()相当の処理を行います。flagがnullならメソッドを呼べずNullPointerExceptionです。

Boolean enabled = null;

if (enabled) { // NullPointerException
    execute();
}

スタックトレースはif行を指すため、条件式にメソッドがなくても例外になります。変数の宣言型と代入元を追います。

Boolean.TRUE.equalsでtrueだけを判定する

nullをfalse側として扱ってよい場合、nullにならない定数側からequalsを呼びます。

if (Boolean.TRUE.equals(enabled)) {
    execute();
}

enabledがtrueのときだけ処理し、falseとnullでは実行しません。ただし、nullが『未回答』を意味し、falseが『明確に拒否』を意味するなら、同じ分岐にまとめてはいけません。

falseを判定するときの注意

Boolean.FALSE.equals(flag)は、flagがfalseのときだけtrueで、nullはfalseになります。!Boolean.TRUE.equals(flag)はfalseとnullの両方でtrueです。似ていますが意味が違います。

Boolean flag = null;

System.out.println(Boolean.FALSE.equals(flag)); // false
System.out.println(!Boolean.TRUE.equals(flag));  // true

否定条件ほどレビューで読み違えます。未設定を含めるのか、明示的なfalseだけなのかをメソッド名や分岐で表します。

二値ならbooleanを選ぶ

必須チェックボックス、内部処理の成功可否、設定済みの機能フラグなど、必ずtrueかfalseならプリミティブbooleanを使うとnull状態を排除できます。

class FeatureSetting {
    private boolean enabled;
}

ただしJSONで項目省略とfalseを区別したい更新APIではBooleanが必要なことがあります。Formや外部DTOではBoolean、Serviceへ渡すコマンドではbooleanというように、境界で意味を確定させます。

DBのNULLとデフォルト値

既存テーブルへフラグ列を追加した際、過去データがNULLになることがあります。Java側だけfalseへ変換すると、DB上の未移行データを隠す場合があります。

Boolean active = entity.getActive();
if (active == null) {
    throw new IllegalStateException("activeが未移行です");
}

NOT NULL制約とデフォルト値を追加できるなら、DBとEntityの契約をそろえます。段階移行中はnullを検知し、移行完了後にBooleanからbooleanへ変える計画も検討します。

JSONの項目省略とfalseを区別する

PATCHのような部分更新では、項目がないことを『変更しない』、falseを『無効化する』と扱うことがあります。この場合Booleanのnullに意味があります。

void apply(UpdateUserRequest request, User user) {
    if (request.active() != null) {
        user.setActive(request.active());
    }
}

単純な新規登録APIで必須なら、Validationでnullを拒否し、内部へ入る前にbooleanへ確定します。APIの種類ごとに契約を分けます。

Optionalで解決するとは限らない

Optionalなら未設定を表せますが、true・false・emptyの3状態がある点は同じです。さらにシリアライズやJPAとの相性、プロジェクト規約もあります。

三値が本当に必要なら、UNANSWERED、ACCEPTED、REJECTEDのenumにすると意味が明確な場合があります。Booleanで表せるかではなく、業務状態を読み手が理解できるかで選びます。

配信同意では、未回答なら入力を促して保存0件、拒否なら利用者だけを登録、許可なら登録後に配信も申し込みます。次のコードは、未回答と拒否を別々の結果として呼び出し元へ返します。

final class ConsentRegistration {
    enum Outcome { ASK_AGAIN, REGISTER_ONLY, REGISTER_AND_SUBSCRIBE }

    Outcome register(Boolean agreed, Runnable saveUser, Runnable subscribe) {
        if (agreed == null) {
            return Outcome.ASK_AGAIN;
        }
        saveUser.run();
        if (Boolean.TRUE.equals(agreed)) {
            subscribe.run();
            return Outcome.REGISTER_AND_SUBSCRIBE;
        }
        return Outcome.REGISTER_ONLY;
    }

    boolean shouldExecute(Boolean enabled) {
        return Boolean.TRUE.equals(enabled);
    }
}

JUnitで三状態をテストする

Booleanを受け取る処理はtrueとfalseだけでなくnullもテストします。nullをfalse扱いするのか、入力エラーにするのか、未変更にするのかを固定します。

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ConsentRegistrationTest {
    private final ConsentRegistration service = new ConsentRegistration();

    @ParameterizedTest
    @CsvSource({
            ", ASK_AGAIN, ''",
            "false, REGISTER_ONLY, save",
            "true, REGISTER_AND_SUBSCRIBE, save/subscribe"
    })
    void keepsThreeConsentOutcomes(Boolean agreed, ConsentRegistration.Outcome expected, String expectedCalls) {
        List<String> calls = new ArrayList<>();

        var actual = service.register(agreed, () -> calls.add("save"), () -> calls.add("subscribe"));

        assertEquals(expected, actual);
        assertEquals(expectedCalls, String.join("/", calls));
    }

    @ParameterizedTest
    @NullSource
    @ValueSource(booleans = {false})
    void doesNotExecuteFeatureWithoutTrue(Boolean enabled) {
        assertFalse(service.shouldExecute(enabled));
    }

    @Test
    void executesFeatureWhenTrue() {
        assertTrue(service.shouldExecute(Boolean.TRUE));
    }
}

現場レビューでよくある指摘

BooleanのレビューではNPE回避だけでなく、nullが不要な状態か、業務上の第三状態かを確認します。

// レビューコメント例
enabledはBooleanなので、null時にif条件のアンボクシングでNPEになります。
二値ならbooleanへ変更してください。

// レビューコメント例
Boolean.TRUE.equalsならnullをfalse側へ寄せられますが、
未回答と拒否を同じにしてよいか仕様を確認してください。

// レビューコメント例
更新APIでは項目省略を「変更なし」と扱うため、Booleanのnullに意味があります。
Controller境界で三状態を明示してください。

NPEを直すためにnullを一律falseへ変換せず、代入元と業務状態を確認します。二値へ狭められる場所ではbooleanを使い、三値が必要な場所だけBooleanを残します。

提出前のセルフチェック

レビュー前に確認すること

  • Booleanがnullになる入口を確認したか
  • if条件でアンボクシングしていないか
  • Boolean.TRUE.equalsが仕様に合うか
  • falseとnullを区別する必要があるか
  • 二値ならbooleanへ変更できないか
  • DB列のNULLとNOT NULL制約を確認したか
  • JSON項目省略とfalseを区別したか
  • true・false・nullをテストしたか

Javaの型とnullを整理する参考書

プリミティブ型、ラッパークラス、オートボクシングを体系的に確認すると、条件式で突然起きるNullPointerExceptionの原因を追いやすくなります。

書籍「スッキリわかるJava入門 第5版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

変数・条件分岐から、クラスの基本まで学ぶ一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

関連記事Javaのnullチェック入門 nullの入口を追い、境界で扱いを決める基本を確認できます。 記事を読む 関連記事JavaのIntegerを==で比較すると結果が変わる理由|equalsとの違い ラッパークラスの参照比較とアンボクシングの別の注意点を解説します。 記事を読む 関連記事Spring Bootの@RequestBodyとは?JSONを受け取る使い方と注意点 JSON項目の省略やnullをFormへ変換する入口を確認できます。 記事を読む

まとめ

Booleanをif条件やboolean変数へ使うと自動アンボクシングが起き、値がnullならNullPointerExceptionになります。trueだけを判定するならBoolean.TRUE.equalsを使えます。

ただし最も重要なのは、nullが不要か第三状態かを決めることです。二値ならboolean、未回答や未変更を表すならBooleanやenumを使い、三状態をテストしてください。

-実装・エラー解決
-