前回のコンパイルエラーは、実行する前に 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 は、id が 1 のときだけ名前を返し、それ以外では 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(...) と書くことはできません。name が null のときに equals を呼べば、length() とまったく同じ理由で落ちるからです。null かもしれない変数に対しては、まだメソッドを呼べません。
落ちない null もある
length() のようなメソッドを呼ばず、+ で文字列につなぐだけなら、エラーになりません。最初のコードの main を、次の1行だけに書き換えて実行してください。
System.out.println("名前: " + findName(2));
名前: null
null が null という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 でした」のほうが、直す場所が特定されます。
次の回へ
次は文字列と数値の変換です。キーボードやフォームから受け取った値は、数字に見えても文字列で届きます。そのままでは計算できないので、変換する形を扱います。