入門 ローカル(VS Code + Java) 約15分

null と NullPointerException を読む

コンパイルは通るのに実行中に止まる NullPointerException を実際に起こし、メッセージから原因と場所を読み取って直すレッスン。

前回のコンパイルエラーは、実行する前に javac が止めてくれるものでした。今回扱うのは、コンパイルは通るのに実行の途中で止まるエラーです。名前は NullPointerException。Java を書いていると何度も出会うので、メッセージを読めるようにしておくと詰まる時間が短くなります。

この回は VS Code とローカルの Java 環境を使います。まだ準備ができていなければ、先にVS Code で Java を書く準備を済ませてください。

null は「値が入っていない」という状態

String のような型の変数には、null という特別な値を入れられます。null は「まだ何も入っていない」「探したが見つからなかった」を表します。

空文字("")とは別です。空文字は「文字が1つも入っていない文字列が、変数に入っている」状態で、null は「文字列そのものが変数に入っていない」状態です。数値の 0 とも別です。

落ちるコードを動かしてみる

Main.java を次の内容にします。

public class Main {
  static String findName(int id) {
    if (id == 1) {
      return "Aki";
    }
    return null;
  }

  public static void main(String[] args) {
    System.out.println("文字数: " + findName(2).length());
  }
}

findName は、id1 のときだけ名前を返し、それ以外では null を返します。main から渡しているのは 2 なので、返るのは null です。length() は文字数を返すメソッドですが、その null に対して呼び出しています。

保存して実行してください。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Main.findName(int)" is null
	at Main.main(Main.java:10)

型として正しいコードでも、実行してみないと分からない失敗があるということです。

メッセージのどこを見るか

長い1行に見えますが、4つの部分に分かれています。

部分意味
java.lang.NullPointerExceptionエラーの種類。null に対して操作しようとした
Cannot invoke "String.length()"失敗した操作。length() を呼ぼうとした
because the return value of "Main.findName(int)" is null原因。findName の戻り値が null だった
at Main.main(Main.java:10)場所。Main.java の10行目。上のコードの System.out.println の行

3つ目が一番の手がかりです。自分で null と書いた覚えがなくても、呼んだメソッドが null を返していれば同じことが起きます

このコードは Temurin 25 で確認しています。because ... の説明が付くのは新しめの Java の機能なので、VS Code で Java を書く準備の手順どおりに入れていれば同じ表示になります。java.lang.NullPointerException の1行しか出ない場合は、別の JDK で動いている可能性があります。その場合も at の行は出るので、まず場所から追ってください。

呼ぶ側で確認して直す

戻り値をいったん変数に入れ、null でないときだけ length() を呼びます。

public class Main {
  static String findName(int id) {
    if (id == 1) {
      return "Aki";
    }
    return null;
  }

  public static void main(String[] args) {
    String name = findName(2);

    if (name != null) {
      System.out.println("文字数: " + name.length());
    } else {
      System.out.println("その ID の人は見つかりませんでした");
    }
  }
}
その ID の人は見つかりませんでした

findName(2)findName(1) に変えて実行すると、今度は 文字数: 3 になります。両方試してみてください。

null かどうかの判定には != を使います。文字列どうしの比較では equals を使いましたが、ここを name.equals(...) と書くことはできません。namenull のときに equals を呼べば、length() とまったく同じ理由で落ちるからです。null かもしれない変数に対しては、まだメソッドを呼べません。

落ちない null もある

length() のようなメソッドを呼ばず、+ で文字列につなぐだけなら、エラーになりません。最初のコードの main を、次の1行だけに書き換えて実行してください。

System.out.println("名前: " + findName(2));
名前: null

nullnull という4文字に置き換わって表示されました。止まらないぶん、気づくのが遅れます。画面やログに null と出ていたら、値が入っていないという意味だと考えてください。

途中まで動いてから落ちる

ArrayList の中身に null が混ざっている場合を見ます。

import java.util.ArrayList;

public class Main {
  public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    names.add("Aki");
    names.add(null);

    for (int i = 0; i < names.size(); i++) {
      System.out.println(names.get(i).length());
    }
  }
}
3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "java.util.ArrayList.get(int)" is null
	at Main.main(Main.java:10)

1件目の Aki は処理され、3 が表示されています。止まったのは2件目です。

コンパイルエラーでは1行も実行されませんが、実行時のエラーは途中まで動いた状態で止まります。どこまで出力されたかが、原因を絞る手がかりになります。

null はどこから来るか

自分で null と書いていなくても、次の経路で入ってきます。

  • 探して見つからなかった — 一覧から条件に合うものを探し、無かったとき
  • 一覧のなかに null が混ざっている — 上の ArrayList の例がこれにあたる。未入力の欄をそのまま入れた場合などに起きる
  • メソッドがそう作られているfindName のように「無ければ null を返す」書き方

3つ目が一番多く、しかも呼ぶ側のコードを見ただけでは分かりません。findName(2) という1行に、null が返る可能性は書かれていないからです。戻り値を使う前に確認するのが基本の対処になります。

AI が出したコードを見るとき

AI に書かせたコードで NullPointerException が出たときは、because の後ろを読んでください。どのメソッドの戻り値が null だったかが書いてあります。

修正を頼むときも、その1行をそのまま渡すのが早いです。「エラーが出ました」より、「Main.findName(int) の戻り値が null でした」のほうが、直す場所が特定されます。

次の回へ

次は文字列と数値の変換です。キーボードやフォームから受け取った値は、数字に見えても文字列で届きます。そのままでは計算できないので、変換する形を扱います。