皆さんこんばんは、駆け出しエンジニアの脆弱性アザラシです。
今回は、Javaの資格であるJava SE11 Silverの試験対策をしていて、対策本の一つである『徹底攻略Java SE 11 Silver問題集』通称「黒本」を読んでいて、NullPointerExceptionが発生するタイミングが気になった問題があり、調べてみました。
デフォルト値によるNullPointerException
今回気になったのは、第5章「配列の操作」の問5と問6の問題です。問5ではまず、下記の問題が挙げられています。
次のプログラムをコンパイル、実行したときの結果として、正しいものを選びなさい。(1つ選択)
public class Item {
String name;
int price = 100;
}
public class Main {
public static void main(String[] args){
Item[] items = new Item[3];
int total = 0;
for (int i = 0; i < items.length; i++) {
total += items[i].price;
}
System.out.println(total);
}
}
志賀澄人、株式会社ソキウス・ジャパン『徹底攻略Java SE 11 Silver問題集』p139より.
この問題の答えは、実行時に例外NullPointerExceptionがスローされる、です。
このコードでなぜ例外が発生するかというと、Mainクラスの3行目では、「3つのアイテムを扱う配列インスタンス」をitemsで宣言するということしかしておらず、Itemのインスタンスを生成しているわけではないからです。ここではオブジェクト型配列のデフォルト値である「null」で初期化されています(1)。
そのため、Mainクラスの6行目でpriceフィールドにアクセスする段階で、変数の参照先が存在しないことを意味するNullPointerExceptionが発生します。
printメソッドの仕様
気になったのが、この次の問題である問6です。
次のプログラムをコンパイル、実行したときの結果として、正しいものを選びなさい。(1つ選択)
public class Main{
public static void main(String[]args){
String[]array = {"A","B","C","D"};
array[0] = null;
for(String str : array){
System.out.print(str);
}
}
}
志賀澄人、株式会社ソキウス・ジャパン『徹底攻略Java SE 11 Silver問題集』p140より.
この問題では、Mainクラスの4行目でarray[0] = null;と、要素の値を変更しています。答えは、「nullBCD」と表示されるということなのですが、先ほどの問5を経た私は、「なぜここではNullPointerExceptionが発生しないんだ…?」と疑問に思いました。
要素の値が変更されているのであれば、配列の一つ目に入っているのは「null」で、そのnullを呼び出そうとしているのだから、NullPointerExceptionが発生するはずでは?と思ったのです。
気になったので、レファレンスを参照してみます。すると、NullPointerExceptionが発生するタイミングは、主に次の5種類であると書かれています。
・nullオブジェクトのインスタンス・メソッドの呼出し。
・nullオブジェクトのフィールドに対するアクセスまたは変更。
・nullの長さを配列であるかのように取得。
・nullのスロットを配列であるかのようにアクセスまたは修正。
・nullをThrowable値であるかのようにスロー(2)。
元々レファレンスにおけるNullPointerExceptionの内容を把握していたわけではありませんが、私が問6で例外がスローされると誤認したのは、一番上の「nullオブジェクトのインスタンス・メソッドの呼出し」だと思います。print()メソッドでnullオブジェクトを呼び出しているんじゃないの?と考えたわけです。
どうにも分からなかったためChatGPTに聞いてみたところ、print()メソッドの仕様が関係している、という回答でした。
黒本の回答の解説(黒本p153)にもある通り、nullはどこも参照していないことを示す「リテラル」として、Javaでは扱われます。
GPTによれば、nullがリテラルであるためSystem.out.println(null) のように null を直接 print() メソッドに渡すことができるということのようです。print()メソッドやprintln()メソッドは、そもそもそういう仕様になっていると……うーむ?
―――
記事を書いている最中に、私と全く同じ疑問を感じて「教えて!goo」にて、で質問をしてらっしゃる方がいるのを発見しました(3)。こちらでもどうやら概ね同じ回答のようでして、そもそもprintメソッドとNullPointerExceptionがそういうものだから、ということのようです。レファレンスでprintメソッドについてさらに調べてみると、このような記述が見つかりました。
public void print(String s)
文字列を出力します。引数がnullの場合は、文字列"null"が出力されます。それ以外の場合、文字列の文字はプラットフォームのデフォルトの文字エンコーディングに従ってバイトに変換され、これらのバイトはwrite(int)メソッドとまったく同じ方法で書き込まれます。
パラメータ:
s - 出力されるString値 (4)
printメソッドはそもそも、nullが来た場合はnullと出力されるように定義されている、だからNullPointerExceptionは発生しないという結論のようです。ちょっと知り切れトンボですが……😅
2024年9月16日.
参考
全体.志賀澄人、株式会社ソキウス・ジャパン『徹底攻略Java SE 11 Silver問題集』2019年,インプレス.
1.やさしいデスマーチ「オブジェクト型配列」、最終閲覧日:2024年9月16日.
2.Java(tm)Platform Standard Edition,"クラスNullPointerException
"、最終閲覧日:2024年9月16日.
3.教えて!goo「Java配列でNullPointerExceptionになるケースとならないケースの差がわかりません」、最終閲覧日:2024年9月16日.
4.Java(tm)Platform Standard Edition,"クラスPrintStream
"、最終閲覧日:2024年9月16日.