java.lang.NullPointerException(NPE、通称「ぬるぽ」)は、null(何も指していない参照)に対してメソッドを呼んだり、フィールドや配列を読んだりしたときに出る例外です。Java 14 以降は、メッセージの because "○○" is null の ○○ が「null だった式」なので、まずそこを見ます。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.items" is null
    at Npe$Cart.add(Npe.java:8)
    at Npe.main(Npe.java:30)

この例では「this.items(フィールド items)が null だったので、List.add を呼べなかった。場所は Npe.java の8行目」と読めます。同じコードを JDK 8 で動かすと、メッセージは java.lang.NullPointerException だけです。 この記事では、よくある原因10パターンを JDK 8・11・17・21・25 で実際に動かし、出力を並べています。

原因別の早見表(メッセージ・典型コード・直し方)

メッセージは JDK 17・21・25 で同じ文面でした。自分のエラーに近い行を探すと、直し方が分かります。

原因 典型コード Java 14 以降のメッセージ(抜粋) 直し方
① 初期化していないフィールド private List<String> items; → items.add(s) because "this.items" is null 宣言時に = new ArrayList<>()、final にしてコンストラクタで代入
② Map.get で無いキー int n = map.get("apple"); Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null map.getOrDefault("apple", 0)
③ 配列の要素が null new String[3] → names[0].length() because "names[0]" is null 要素を代入してから使う。Arrays.fill
④ オートアンボクシング Integer price = null; int t = price + 100; Cannot invoke "java.lang.Integer.intValue()" because "price" is null 基本型 int で持つ。null なら既定値を決める
⑤ メソッドの戻り値が null findUser(id).name() because the return value of "Npe.findUser(String)" is null 戻り値を Optional にする。呼ぶ側で分岐
⑥ Spring の @Autowired が効かない new UserService().nameOf(id) because "this.userRepository" is null new しない。コンストラクタインジェクション
⑦ JPA のエンティティ・遅延ロード order.getCustomer().getName() because the return value of "Order.getCustomer()" is null など 関連が null になり得るか確かめる。equals はゲッターで比較
⑧ Stream の途中に null .map(String::toUpperCase) メッセージなし(メソッド参照の場合) filter(Objects::nonNull)。toMap の値の null に注意
⑨ equals の向き status.equals("ACTIVE") Cannot invoke "String.equals(Object)" because "status" is null "ACTIVE".equals(status) か Objects.equals
⑩ toString と String.valueOf o.toString() Cannot invoke "Object.toString()" because "o" is null String.valueOf(o) は "null" を返す

確認環境:OpenJDK 8u504・11.0.32.1・17.0.20.1・21.0.10・25.0.4.1(Ubuntu 24.04)で再現コードを javac -g でコンパイルして実行し、出力をそのまま載せています。⑥は Spring Framework 4.3.30(Ubuntu のパッケージ)と JDK 25、静的解析は SpotBugs 4.10.5 と JSpecify 1.0(JDK 21)で実行しました。Helpful NullPointerException の仕様は JEP 358、Spring の null 安全は Spring Framework リファレンス、JPA の遅延ロードは Hibernate ORM のドキュメント、IDE の機能は IntelliJ IDEA のヘルプで確かめました(確認日 2026年10月16日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「NullPointerExceptionの原因と対処法|原因別早見表【Java】」

NullPointerException のエラーメッセージの読み方

3か所を順に見る

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Npe$UserRepository.findById(String)" because "this.userRepository" is null
    at Npe$UserService.nameOf(Npe.java:24)
    at Npe.main(Npe.java:56)
順番 見る場所 この例で分かること
1 because "○○" is null の ○○ null だったのは this.userRepository(フィールド)
2 Cannot invoke "△△" の △△ 呼ぼうとしたのは UserRepository.findById(String)
3 最初の at 行(自分のコード) 起きた場所は Npe.java の24行目、UserService.nameOf の中
4 その下の at 行 呼び出し元。Npe.main の56行目から呼ばれた

直すのは「null を使った24行目」ではなく、「null のまま渡してきた場所」であることが多い点に注意します。この例では、userRepository に値を入れるはずの処理(DI)が動いていないことが原因です。

メッセージの種類(Java 14 以降)

JDK 25 で実行して出たメッセージです。前半で「何をしようとしたか」、後半で「何が null だったか」が分かります。

メッセージの前半 何をしようとしたか 例
Cannot invoke "..." メソッド呼び出し Cannot invoke "String.length()" because "names[0]" is null
Cannot read field "..." フィールドの読み取り Cannot read field "count" because "b.next" is null
Cannot assign field "..." フィールドへの代入 Cannot assign field "count" because "box" is null
Cannot load from ... array 配列の要素の読み取り Cannot load from int array because "arr" is null
Cannot store to ... array 配列の要素への代入 Cannot store to int array because "arr" is null
Cannot read the array length 配列.length Cannot read the array length because "arr" is null
Cannot enter synchronized block synchronized (x) Cannot enter synchronized block because "lock" is null
Cannot throw exception throw x Cannot throw exception because "ex" is null

後半の書き方は次の3種類です。

後半 意味
because "price" is null ローカル変数・引数 price が null
because "this.items" is null フィールド items が null
because the return value of "Npe.findUser(String)" is null メソッド findUser が null を返した

JDK 8・11 と JDK 17・21・25 のメッセージの違い

同じコード(後述の Npe.java)を各 JDK でコンパイルして実行した結果です。

JDK ①未初期化フィールドのメッセージ
8(1.8.0_504) java.lang.NullPointerException(説明なし)
11(11.0.32.1) java.lang.NullPointerException(説明なし)
17(17.0.20.1) Cannot invoke "java.util.List.add(Object)" because "this.items" is null
21(21.0.10) 同上
25(25.0.4.1) 同上

この機能は Helpful NullPointerExceptions(JEP 358)といい、Java 14 で入りました。JEP 358 では、Java 14 の時点では既定で無効(-XX:+ShowCodeDetailsInExceptionMessages で有効)とされています。Java 15 から既定で有効になり、JDK 17・21・25 ではオプションなしでメッセージが出ました。JDK 25 で -XX:-ShowCodeDetailsInExceptionMessages を付けると、JDK 8 と同じく説明のないメッセージに戻りました。JDK 8・11 でこのオプションを付けると Unrecognized VM option 'ShowCodeDetailsInExceptionMessages' で起動しません。

JDK 8・11 でメッセージに説明がないときは、行番号の行にある「ドット(.)の左側」と「配列の [ の左側」と「アンボクシングされる値」を1つずつ疑います。 1行に a.getB().getC() のような連鎖があると切り分けにくいため、変数に分けて書くと行番号で特定できます。手元の Java の版の確かめ方は、関連記事「Javaバージョン一覧【2026年】LTSのサポート期限とJava 27・28」で扱っています。

変数名ではなく <local4> と出るとき

javac に -g(デバッグ情報を付ける)を付けずにコンパイルすると、ローカル変数の名前がクラスファイルに残らず、番号で表示されます。JDK 25 での結果です。

コンパイル メッセージ
javac -g Cannot read the array length because "scores" is null
javac(-g なし) Cannot read the array length because "<local4>" is null

フィールド(this.items)とメソッドの戻り値は、-g がなくても名前が出ました。JEP 358 にも、ローカル変数名はクラスファイルにローカル変数表があるときだけ表示され、ないときは <local i> や <parameter i> になると書かれています。Maven や Gradle の既定のビルドではデバッグ情報が付くので、多くの場合は変数名が出ます。

メッセージに説明が付かない NullPointerException

Helpful NullPointerExceptions の説明が付くのは、JVM が自分で投げた NPE だけです(JEP 358)。ライブラリのコードが Objects.requireNonNull などで投げた NPE は、JDK 25 でもメッセージなしでした。

起きた場所 JDK 25 のメッセージ 見るところ
Optional.of(null) なし(at java.util.Objects.requireNonNull → at java.util.Optional.of) スタックトレースで最初に出てくる自分のコードの行
Collectors.toMap の値が null なし(at java.util.Objects.requireNonNull → Collectors.lambda$uniqKeysMapAccumulator$0) 同上
Stream のメソッド参照(String::toUpperCase) なし。行番号は終端操作(collect)の行 ラムダ式に書き換えると変数名が出る(後述)
Objects.requireNonNull(email, "email は必須です") email は必須です 自分で付けたメッセージ

何度も起きるとスタックトレースが消える(OmitStackTraceInFastThrow)

本番のログで java.lang.NullPointerException の1行だけで、at 行が1行もないことがあります。HotSpot(標準の JVM)は、同じ場所で何度も起きる例外を最適化して、スタックトレースのない例外を使い回します。同じ箇所で NPE を10万回起こして数えた結果です(数は実行のたびに変わります)。

public class FastThrow {
    static int len(String s) { return s.length(); }
    public static void main(String[] args) {
        int withTrace = 0, noTrace = 0;
        String last = null;
        for (int i = 0; i < 200_000; i++) {
            try {
                len(i % 2 == 0 ? "abc" : null);
            } catch (NullPointerException e) {
                if (e.getStackTrace().length == 0) { noTrace++; last = String.valueOf(e.getMessage()); }
                else withTrace++;
            }
        }
        System.out.println("スタックトレースあり=" + withTrace + " なし=" + noTrace + " 最後のメッセージ=" + last);
    }
}
JDK 既定 OmitStackTraceInFastThrow を無効にした場合
8 あり=46593 なし=53407 あり=100000 なし=0
17 あり=45279 なし=54721 あり=100000 なし=0
21 あり=73070 なし=26930 あり=100000 なし=0
25 あり=49043 なし=50957 あり=100000 なし=0

スタックトレースのない NPE は、メッセージも null でした(JDK 25 でも同じ)。 ログに at 行がないときは、同じ NPE の最初の発生をログの前のほうから探すか、起動オプションに -XX:-OmitStackTraceInFastThrow を付けて再現させます。

スタックトレースのどの行を見るか

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springNpe.ReportService': Instantiation of bean failed; nested exception is ...
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1158)
    ...(Spring の行が続く)
    at SpringNpe.main(SpringNpe.java:43)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [SpringNpe$ReportService]: Constructor threw exception; nested exception is ...
    ...
Caused by: java.lang.NullPointerException: Cannot invoke "SpringNpe$UserRepository.findName(String)" because "this.userRepository" is null
    at SpringNpe$ReportService.<init>(SpringNpe.java:24)
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
    ...
状況 見る行
Caused by: がない 1行目のメッセージと、最初に出てくる自分のパッケージの at 行
Caused by: がある いちばん下の Caused by: が根本の原因。その直後の自分のコードの at 行
at java.base/... や at org.springframework... が先頭に続く それらを読み飛ばし、自分のクラスが最初に出る行を探す
<init> と出る コンストラクタの中で起きている
lambda$main$0 と出る ラムダ式の中で起きている。行番号はラムダの中の行
... 13 more 上の例外と同じ行が続くので省略された、という意味

スタックトレースの読み方は、関連記事「Javaスタックトレースの読み方|Caused byの意味と原因の探し方」で扱っています。

原因10パターンを再現コードと出力で確かめる

次の Npe.java を JDK 8 でも動くように書き、引数で原因を切り替えて実行しました。出力の行番号はこのファイルの行番号です。

import java.util.*;
import java.util.stream.*;

public class Npe {
    // 1. 未初期化フィールド
    static class Cart {
        private List<String> items;              // new していない
        void add(String s) { items.add(s); }
    }
    // 5. メソッドの戻り値で使うクラス
    static final class User {
        private final String id, name;
        User(String id, String name) { this.id = id; this.name = name; }
        String id() { return id; }
        String name() { return name; }
    }
    static User findUser(String id) {
        return "u1".equals(id) ? new User("u1", null) : null;   // 見つからないと null
    }
    // 6. DI されないフィールド(Spring の @Autowired が効かない状況と同じ形)
    static class UserRepository { User findById(String id) { return findUser(id); } }
    static class UserService {
        UserRepository userRepository;           // DI で入る想定。new すると null のまま
        String nameOf(String id) { return userRepository.findById(id).name(); }
    }

    public static void main(String[] args) {
        String c = args[0];
        switch (c) {
            case "field": new Cart().add("ペン"); break;
            case "mapget": {
                Map<String, Integer> stock = new HashMap<>();
                stock.put("pen", 3);
                int n = stock.get("apple");     // null を int に代入
                break; }
            case "array": {
                String[] names = new String[3];  // 要素はすべて null
                System.out.println(names[0].length());
                break; }
            case "arraynull": {
                int[] scores = null;
                System.out.println(scores.length);
                break; }
            case "unbox": {
                Integer price = null;
                int total = price + 100;
                break; }
            case "ternary": {
                Integer discount = null;
                boolean member = false;
                Integer d = member ? 10 : discount;   // 三項演算子の自動アンボクシング
                System.out.println(d);
                break; }
            case "return": System.out.println(findUser("u9").name()); break;
            case "chain": System.out.println(findUser("u1").name().length()); break;
            case "autowired": System.out.println(new UserService().nameOf("u1")); break;
            case "stream": {
                List<User> users = Arrays.asList(new User("u1", "sato"), new User("u2", null));
                System.out.println(users.stream().map(User::name).map(String::toUpperCase).collect(Collectors.toList()));
                break; }
            case "tomap": {
                List<User> users = Arrays.asList(new User("u1", "sato"), new User("u2", null));
                System.out.println(users.stream().collect(Collectors.toMap(User::id, User::name)));
                break; }
            case "optionalof": { String s = null; System.out.println(Optional.of(s)); break; }
            case "equals": {
                String status = null;
                System.out.println("ACTIVE".equals(status));
                System.out.println(Objects.equals(status, "ACTIVE"));
                System.out.println(status.equals("ACTIVE"));
                break; }
            case "tostring": {
                Object o = null;
                System.out.println(String.valueOf(o));
                System.out.println("値=" + o);
                System.out.println(o.toString());
                break; }
            case "valueofchar": { System.out.println(String.valueOf((char[]) null)); break; }
            case "switch": {
                String s = null;
                switch (s) { case "A": System.out.println("A"); break; default: System.out.println("other"); }
                break; }
            case "require": {
                String email = null;
                Objects.requireNonNull(email, "email は必須です");
                break; }
            default: System.out.println("?");
        }
    }
}
javac -g -d out Npe.java
java -cp out Npe field      # 引数を mapget・array・unbox などに変えて実行

① 初期化していないフィールド

$ java -cp out Npe field       # JDK 8
Exception in thread "main" java.lang.NullPointerException
    at Npe$Cart.add(Npe.java:8)
    at Npe.main(Npe.java:30)

$ java -cp out Npe field       # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.items" is null
    at Npe$Cart.add(Npe.java:8)
    at Npe.main(Npe.java:30)

参照型のフィールドの初期値は null です。this. が付いていたらフィールドが原因です。

static class Cart {
    private final List<String> items = new ArrayList<>();   // 宣言時に初期化。final で再代入も防ぐ
    void add(String s) { items.add(s); }
}

② Map.get で存在しないキー(アンボクシング)

$ java -cp out Npe mapget      # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
    at Npe.main(Npe.java:34)

コードには intValue() を書いていないのに、メッセージに Integer.intValue() が出ます。 これは int n = map.get(...) の代入で、コンパイラが自動で intValue() を呼ぶコード(アンボクシング)を入れているためです。

int n = stock.getOrDefault("apple", 0);   // 実行結果:apple=0

getOrDefault や merge の使い方は、関連記事「Java HashMapの使い方|ループ・getOrDefault・merge早見表」で扱っています。

③ 配列の要素が null・配列そのものが null

$ java -cp out Npe array       # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "names[0]" is null
    at Npe.main(Npe.java:38)

$ java -cp out Npe arraynull   # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "scores" is null
    at Npe.main(Npe.java:42)

new String[3] は「null が3つ入った配列」です。names[0] のように [ ] 付きで出たら要素、Cannot read the array length なら配列そのものが null です。要素は代入してから使い、Arrays.fill(names, "") で空文字を入れておく方法もあります。

④ オートアンボクシング(Integer → int)と三項演算子

$ java -cp out Npe unbox       # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "price" is null
    at Npe.main(Npe.java:46)

$ java -cp out Npe ternary     # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "discount" is null
    at Npe.main(Npe.java:51)

三項演算子の例は、代入先が Integer なのに NPE になります。 member ? 10 : discount の片方が int(10)なので、式全体の型が int になり、discount がアンボクシングされるためです。両方を Integer にそろえる(member ? Integer.valueOf(10) : discount)と、null がそのまま代入されます。Integer と int の違いは、関連記事「Java Integerとintの違い|ラッパークラスとオートボクシング」で扱っています。

⑤ メソッドの戻り値が null

$ java -cp out Npe return      # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Npe$User.name()" because the return value of "Npe.findUser(String)" is null
    at Npe.main(Npe.java:54)

$ java -cp out Npe chain       # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Npe$User.name()" is null
    at Npe.main(Npe.java:55)

findUser("u1").name().length() のような連鎖でも、どのメソッドが null を返したか(User.name())まで特定できます。 JDK 8・11 では同じ行番号しか出ないため、findUser と name() のどちらかは分かりません。直し方は、戻り値を Optional にする方法です(後述の「予防」)。

⑥ Spring の @Autowired が効かない(null のまま)

Spring Framework 4.3.30 と JDK 25 で、次のコードを実行しました。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

public class SpringNpe {
    @Component
    public static class UserRepository {
        public String findName(String id) { return "sato"; }
    }

    @Component
    public static class UserService {
        @Autowired
        private UserRepository userRepository;
        public String nameOf(String id) { return userRepository.findName(id); }
    }

    @Component
    public static class ReportService {
        @Autowired
        private UserRepository userRepository;
        public ReportService() {
            // コンストラクタの時点では、フィールドへの注入はまだ行われていない
            System.out.println("init: " + userRepository.findName("u1"));
        }
    }

    @Component
    public static class FixedService {
        private final UserRepository userRepository;
        public FixedService(UserRepository userRepository) {   // コンストラクタインジェクション
            this.userRepository = userRepository;
            System.out.println("init: " + userRepository.findName("u1"));
        }
    }

    public static void main(String[] args) {
        String c = args[0];
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(UserRepository.class, UserService.class);
        if (c.equals("ctor")) ctx.register(ReportService.class);
        if (c.equals("fixed")) ctx.register(FixedService.class);
        ctx.refresh();
        if (c.equals("bean")) System.out.println("getBean: " + ctx.getBean(UserService.class).nameOf("u1"));
        if (c.equals("new")) System.out.println("new: " + new UserService().nameOf("u1"));
        ctx.close();
    }
}
引数 何をしたか 結果
bean Spring から取り出した UserService を使う getBean: sato
new 自分で new UserService() した NullPointerException: Cannot invoke "SpringNpe$UserRepository.findName(String)" because "this.userRepository" is null
ctor コンストラクタの中で @Autowired のフィールドを使う BeanCreationException。いちばん下の Caused by: が上と同じ NPE(at SpringNpe$ReportService.<init>)
fixed コンストラクタインジェクションに変えた init: sato

@Autowired は、Spring が作ったオブジェクトにだけ値を入れます。 自分で new したオブジェクトには何も入らず、null のままです。また、フィールドへの注入はコンストラクタが終わった後に行われるので、コンストラクタの中ではまだ null です。

症状 原因 直し方
new ○○Service() した先で NPE Spring の管理外のオブジェクト new せず、使う側にも注入する
コンストラクタの中で NPE(<init> の行) フィールド注入はコンストラクタの後 コンストラクタインジェクションにする
static フィールドに @Autowired Spring は static フィールドに注入しない static をやめる
クラスに @Component などがない・スキャン対象のパッケージの外 Bean として登録されていない アノテーションとパッケージ構成を確かめる(多くは起動時に別のエラーになる)

Spring Framework 6 以降は、例外メッセージに nested exception is ... が付かない形に変わっていますが、Caused by: をたどる読み方は同じです。DI の書き方は、関連記事「Spring DIとは|@Autowiredとコンストラクタインジェクション」で扱っています。

⑦ JPA のエンティティと遅延ロード

JPA(Java のデータベース操作の標準仕様)では、次の3つの形で null が出てきます。ここは Hibernate ORM のドキュメントをもとに整理しています。

状況 なぜ null か 直し方
new Order() したエンティティの getItems().add(item) @OneToMany のコレクションを初期化していない(原因①と同じ) private List<Item> items = new ArrayList<>(); と宣言時に初期化
order.getCustomer().getName() 外部キーが NULL の行では、任意(optional)の @ManyToOne は null 関連が必須なら optional = false と NOT NULL 制約。任意なら null を前提に分岐
equals で other.name を直接読む 遅延ロードのプロキシは、状態を読み込むまでフィールドに値を持たない equals・hashCode ではゲッター(other.getName())を使い、型は instanceof で確かめる

Hibernate のドキュメントでは、プロキシは「本物のエンティティのふりをするが、まだデータベースから状態を取ってきていないので状態を持たないオブジェクト」と説明されています。メソッドを呼ぶと、そこで状態を読み込みます。そのため equals() で相手のフィールドを直接読むと、プロキシの空のフィールド(null)を読むことになります。ドキュメントは、equals() では getClass() ではなく instanceof で型を確かめ、相手のフィールドはアクセサメソッド経由で読むよう注意しています。

@Override
public boolean equals(Object o) {
    return o instanceof Book other              // getClass() ではなく instanceof
        && getIsbn().equals(other.getIsbn());   // other.isbn ではなくゲッター
}

なお、セッション(永続化コンテキスト)が閉じた後にプロキシのメソッドを呼ぶと、NPE ではなく LazyInitializationException になります。必要な関連は JOIN FETCH などで先に読み込んでおく方法があり、関連記事「JPAのN+1問題の解決|JOIN FETCH等4手法の比較とコード全文」で扱っています。

⑧ Stream の途中に null がある

$ java -cp out Npe stream      # JDK 25
Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
    at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024)
    ...

$ java -cp out Npe tomap       # JDK 25
Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:220)
    at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$0(Collectors.java:180)
    ...

Stream の NPE は、JDK 25 でもメッセージが出ないことがあります。 メソッド参照(String::toUpperCase)とラムダ式(n -> n.toUpperCase())を、別のファイル(StreamNpe.java)で比べました。

List<User> users = Arrays.asList(new User("u1", "sato"), new User("u2", null));

users.stream()
     .map(User::name)
     .map(String::toUpperCase)        // メソッド参照
     .collect(Collectors.toList());   // ← 17行目

users.stream()
     .map(User::name)
     .map(n -> n.toUpperCase())       // ラムダ式 ← 21行目
     .collect(Collectors.toList());
書き方 JDK 8 JDK 25
メソッド参照 メッセージなし。自分のコードの行は StreamNpe.main(StreamNpe.java:17)(collect の行) 同じ(メッセージなし、17行目)
ラムダ式 メッセージなし。StreamNpe.lambda$main$0(StreamNpe.java:21) Cannot invoke "String.toUpperCase()" because "n" is null、lambda$main$0(StreamNpe.java:21)

メソッド参照で NPE が出たら、一時的にラムダ式に書き換えると、どの要素の何が null かが分かります。 行番号が map ではなく collect の行を指すことにも注意します。

Stream の操作 null の扱い 対処
map(String::toUpperCase) 要素が null だと NPE 前に filter(Objects::nonNull)
Collectors.toMap 値が null だと NPE(キーの重複がなくても) null を除くか、collect(HashMap::new, (m, u) -> m.put(...), HashMap::putAll)
Optional.of(x) x が null だと NPE Optional.ofNullable(x)
List.of(...)・Map.of(...) null を入れると NPE null を入れない設計にする

filter(Objects::nonNull) と、HashMap::new を使った collect に直した結果です(JDK 8・25 とも同じ)。

[SATO]
{u1=sato, u2=null}

toMap のキー重複のエラーは、関連記事「Java ListをMapに変換する方法 toMapのキー重複エラー対処」で扱っています。

⑨ equals の向き

$ java -cp out Npe equals      # JDK 25
false
false
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "status" is null
    at Npe.main(Npe.java:70)
書き方 status が null のとき
"ACTIVE".equals(status) false
Objects.equals(status, "ACTIVE") false(両方 null なら true)
status.equals("ACTIVE") NPE

定数を左に書くか、Objects.equals を使います。 switch (status) も、status が null だと NPE になります(JDK 25 で Cannot invoke "String.hashCode()" because "<local5>" is null)。文字列の switch はコンパイラが作る一時変数を使うため、-g を付けても変数名ではなく <local5> と出ました。文字列比較の詳細は、関連記事「Javaの文字列比較|equalsと==の違いと正しい比べ方」で扱っています。

⑩ String.valueOf と toString

$ java -cp out Npe tostring    # JDK 25
null
値=null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Object.toString()" because "o" is null
    at Npe.main(Npe.java:76)

$ java -cp out Npe valueofchar # JDK 25
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "value" is null
    at java.base/java.lang.String.<init>(String.java:286)
    at java.base/java.lang.String.valueOf(String.java:4547)
    at Npe.main(Npe.java:78)
書き方 o が null のとき
String.valueOf(o)(Object 型) 文字列 "null"
"値=" + o "値=null"
o.toString() NPE
String.valueOf((char[]) null) NPE(char[] を受け取るメソッドが選ばれる)

String.valueOf は、char[] 型の null を渡すと NPE になります。 引数に直接 null と書いた場合も、コンパイラは valueOf(char[]) を選びます。メッセージの "value" は JDK の中の引数名で、スタックトレースの Npe.main(Npe.java:78) が自分のコードの行です。

NullPointerException の予防

予防策の早見表

方法 いつ止まるか 向いている場面
Objects.requireNonNull(x, "メッセージ") 実行時、値を受け取った瞬間 コンストラクタ・公開メソッドの引数チェック
Optional を戻り値にする コンパイル時(扱いを書かないと値を取り出せない) 「見つからないかもしれない」検索メソッド
record + コンパクトコンストラクタ 実行時、オブジェクトを作る瞬間 値オブジェクト・DTO
JSpecify の @NullMarked・@Nullable IDE・静的解析の時点 チーム開発、ライブラリの公開 API
SpotBugs・IntelliJ IDEA の検査 ビルド時・編集中 CI での自動チェック

Objects.requireNonNull で「渡された瞬間」に止める

record User(String id, String name) {
    User {
        Objects.requireNonNull(id, "id は必須です");   // 作る時点で止める
    }
}
record: id は必須です

(new User(null, "x") の NPE を catch して getMessage() を表示した結果。JDK 21・25)

null を使う場所ではなく、null が入ってきた場所で止まるので、スタックトレースが原因の場所を直接指します。null のときに既定値を使う場合は、Java 9 以降の Objects.requireNonNullElse(x, 既定値) が使えます。null チェックの書き方は、関連記事「Java nullチェックの書き方|requireNonNullとisNull」で扱っています。

Optional で「無いかもしれない」を型にする

static Optional<User> findById(String id) {
    return Optional.ofNullable(DB.get(id));           // 見つからないと Optional.empty()
}

findById("u1").map(User::name).orElse("(未登録)");   // sato
findById("u9").map(User::name).orElse("(未登録)");   // (未登録)
findById("u9").get();                                  // NoSuchElementException: No value present

Optional は戻り値に使います。 フィールドや引数には使わないのが一般的です。中身を確かめずに get() を呼ぶと、NPE の代わりに NoSuchElementException になります(JDK 25 で java.util.NoSuchElementException: No value present)。orElse と orElseGet の違いは、関連記事「Java Optionalの使い方|orElseとorElseGetの違いと使う場所」で扱っています。

JSpecify の @Nullable と SpotBugs で、動かす前に見つける

JSpecify は、null を許すかどうかを示すアノテーションの共通仕様です。@NullMarked を付けたクラスやパッケージでは「何も付いていない型は null を許さない」とし、null を許す所にだけ @Nullable を付けます。Spring Framework 7 は JSpecify のアノテーションを採用し、org.springframework.lang の @Nullable などを非推奨にしています(Spring Framework リファレンス)。

null の扱いを間違えたコードを書き、SpotBugs 4.10.5(静的解析ツール)で検査しました。

import java.util.Map;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class UserFinder {
    private final Map<String, String> names;
    public UserFinder(Map<String, String> names) { this.names = names; }

    // 見つからないと null を返す
    public @Nullable String find(String id) {
        return names.get(id);
    }

    // 1. null を返し得るメソッドの戻り値を、確かめずに使う
    public int nameLength(String id) {
        return find(id).length();
    }

    // 2. null チェックの後で、null の経路でも使ってしまう
    public String label(@Nullable String name) {
        if (name == null) {
            System.out.println("名前がありません");
        }
        return name.trim();
    }

    // 3. null を確実に参照する
    public int alwaysNull() {
        String s = null;
        return s.length();
    }
}
$ spotbugs -textui -effort:max -low -auxclasspath jspecify-1.0.1.jar c
H C NP: Possible null pointer dereference of name in UserFinder.label(String)  Dereferenced at UserFinder.java:[line 25]
H C NP: Null pointer dereference of s in UserFinder.alwaysNull()  Dereferenced at UserFinder.java:[line 31]
M D NP: Possible null pointer dereference in UserFinder.nameLength(String) due to return value of called method  Dereferenced at UserFinder.java:[line 17]
L D NP: Load of known null value in UserFinder.alwaysNull()  At UserFinder.java:[line 31]

(null 以外の指摘 EI_EXPOSE_REP2 が1件出ましたが、省略しています)

行 SpotBugs のバグパターン @Nullable を外した場合
17(戻り値を確かめずに使う) NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE 検出されなかった
25(null の経路で使う) NP_NULL_ON_SOME_PATH 検出された
31(必ず null) NP_ALWAYS_NULL、NP_LOAD_OF_KNOWN_NULL_VALUE 検出された

find に @Nullable を付けたときだけ、17行目の「戻り値を確かめずに使う」が検出されました。 メソッドの戻り値が null になり得ることは、アノテーションで示さないとツールに伝わりません。SpotBugs は Maven・Gradle のプラグインからも実行でき、CI に組み込めます。

IntelliJ IDEA では、設定の Editor → Inspections → Probable bugs にある「Nullability and data flow problems」が、null になり得る変数の参照を警告します。IntelliJ IDEA のヘルプによると、既定で認識するアノテーションには JSpecify の @Nullable・@NonNull が含まれます。Spring のリファレンスでは、ビルド時に強制するツールとして NullAway も挙げられています。

コードレビューで見るところ

見るところ 置き換え
return null; しているメソッド Optional、空のコレクション(List.of())、空文字
Map.get の結果を int などに代入 getOrDefault
Integer と int が混ざる三項演算子 両辺の型をそろえる
変数.equals("定数") "定数".equals(変数)、Objects.equals
自分で new している Spring の Bean 注入する
@Autowired のフィールドをコンストラクタで使う コンストラクタインジェクション
Optional.get() を確かめずに呼ぶ orElse・orElseThrow
Collectors.toMap の値が null になり得る null を除く、別の collect にする

よくある質問

Q. java.lang.NullPointerException とは何ですか? A. null の参照に対して、メソッド呼び出し・フィールドの読み書き・配列の操作などをしたときに JVM が投げる例外です。RuntimeException のサブクラス(非検査例外)なので、throws に書かなくてもコンパイルが通り、実行して初めて分かります。ほかの例外との違いは、関連記事「Java 例外一覧|種類・原因・対処の早見表と使い分け」で扱っています。

Q. NullPointerException のメッセージで、どこを見ればよいですか? A. because "○○" is null の ○○ と、最初に出てくる自分のコードの at 行です。○○ が null だった式、at 行が起きた場所です。Caused by: がある場合は、いちばん下の Caused by: を見ます。

Q. メッセージに「Cannot invoke」が出ないのはなぜですか? A. JDK 8・11 を使っているか、JDK の中のコード(Objects.requireNonNull など)が投げた NPE だからです。java -version で版を確かめます。同じ場所で何度も起きた NPE は、スタックトレースもメッセージも消えることがあり、-XX:-OmitStackTraceInFastThrow で確かめられます。

Q. null exception と NullPointerException は同じですか? A. Java で null が原因の例外は NullPointerException です。C# では NullReferenceException、JavaScript では TypeError: Cannot read properties of null など、言語によって名前が違います。Kotlin では型で null を区別でき、関連記事「Kotlin null安全の使い方|?. ?: !! letの違い早見表」で扱っています。

Q. NullPointerException を catch すれば解決しますか? A. catch するより、null が入ってくる原因を直すのが基本です。 catch で握りつぶすと、どこで null が生まれたかが分からなくなります。外部から来るデータは入口で検証し、内側のコードは null が来ない前提で書くと、!= null のチェックが減ります。

Q. 「ぬるぽ」とは何ですか? A. NullPointerException の日本での通称です。意味は同じです。

まとめ

  • メッセージの because "○○" is null が、null だった式。this.○○ はフィールド、the return value of はメソッドの戻り値
  • JDK 8・11 ではメッセージが出ない。JDK 15 以降は既定で出る(JDK 17・21・25 で同じ文面)
  • -g なしでコンパイルすると変数名は <local4> になる。JDK 内部が投げた NPE と、Stream のメソッド参照ではメッセージが出ない
  • 何度も起きた NPE は スタックトレースが消える。-XX:-OmitStackTraceInFastThrow で再現できる
  • 原因は ①未初期化フィールド ②Map.get ③配列の要素 ④アンボクシング(三項演算子を含む) ⑤戻り値 ⑥@Autowired に new・コンストラクタ内での利用 ⑦JPA の関連とプロキシ ⑧Stream と toMap ⑨equals の向き ⑩toString と String.valueOf(char[])
  • 予防は Objects.requireNonNull・Optional・record・JSpecify の @Nullable と SpotBugs。@Nullable を付けると、戻り値の確かめ忘れを SpotBugs が検出した

参考・出典

確認日はいずれも 2026年10月16日です。

  • JEP 358: Helpful NullPointerExceptions:https://openjdk.org/jeps/358
  • Java SE 25 API NullPointerException:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/NullPointerException.html
  • Java SE 25 API Objects:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Objects.html
  • Java SE 25 API Optional:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Optional.html
  • Spring Framework リファレンス「Null-safety」:https://docs.spring.io/spring-framework/reference/core/null-safety.html
  • Hibernate ORM「An Introduction to Hibernate」(Proxies and lazy fetching/equals() and hashCode()):https://docs.hibernate.org/orm/7.0/introduction/html_single/
  • JSpecify:https://jspecify.dev/
  • SpotBugs バグの説明:https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html
  • SpotBugs リリース:https://github.com/spotbugs/spotbugs/releases
  • IntelliJ IDEA ヘルプ「Annotating source code」:https://www.jetbrains.com/help/idea/annotating-source-code.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「NullPointerExceptionの原因と対処法|原因別早見表【Java】」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?