はじめに
この記事はJava Advent Calendar 2020の19日目の記事です。タイトルはふざけていますが、内容は普通です。たぶん。きっと。
最近寒いですね。こんな寒い時は暖かいコーヒーに限ります。Javaプログラマはみんなコーヒーが好きです[要出典]。というわけで皆さんもコーヒーがお好きだと思いますが、どんなコーヒーがお好きですか?私はここのところはセブンイレブンのグアテマラブレンドを毎日飲んでいるのですが。
さて、突然ですがJavaにおいてnull
をキャストすると何が起こると思いますか?
何も起こらない?そうかもしれないですが、そうでもない場合があります。だいぶ地味な話題ですが、興味があればお付き合いください。
サンプルコードの実行環境
- macOS Big Sur 11.0.1(20B29)
- Apple M1
- Java 13.0.5.1 (Azul Zulu)
基本
まず、最もシンプルなケースについて見てみます。null
リテラルを何かにキャストするとどうなるでしょうか?
System.out.println((String) null);
System.out.println("done");
実行結果
null
done
何のエラーもなく実行されました。キャストしただけではNullPointerExecption
もClassCastExeption
も発生しません。
なんでそんなことする必要があるんですかね…
そもそもnull
リテラルをキャストしたいことなんてあるのか?という話ではあるんですが、まあ普通はないですね。でも全くないとは限りません。
たとえば、上の例でキャストを外すとどうなるでしょうか。
System.out.println(null);
System.out.println("done");
すると、実行結果以前にコンパイルが通りません。以下のようなコンパイルエラーが出力されます。
java: printlnの参照はあいまいです
java.io.PrintStreamのメソッド println(char[])とjava.io.PrintStreamのメソッドprintln(java.lang.String)の両方が一致します
引数の型がchar[]
なのかString
なのかわからんって言ってますね。println()
はオーバーロードしていて、char[]
型を引数に取るものもString
型を引数に取るものもあり、null
リテラルではどちらとも解釈できてしまうからどっちを呼べばいいかコンパイラには判断できずにエラーとなるわけですね。1
そこでキャストの出番です。キャストすればどの型なのかはっきりしますので、コンパイラもどのメソッドを呼ぶのか判断でき、コンパイルが通って実行もできるというわけです。
それ以前の話
いやいや、そもそもサンプルコード以外でnull
リテラルを渡したいことなんてあるのかというツッコミが入るかもしれません。まあ普通はないですね。でも全くないとは限りません。たとえば、引数としてnull
が渡された場合に例外を投げるメソッドはよくありますので、それに対するテストコードではnull
リテラルを渡すことがあり得ます。
それで、前述のようなエラーやその他警告などが出た場合は、解決策の一つとしてnull
をキャストするということがあるわけです。2
キャストにより値が変わってしまうケース
さて、上記の例におけるキャストはnull
を何の型として解釈すべきなのかをただコンパイラに教えてあげるだけのもので、キャストによってnull
が何か別の値に変換されるわけではありませんでした。null
リテラルに限らず、参照型におけるキャストとは一般的にそういうものです。3
しかし、なんとnull
リテラルをキャストした時に、null
がnull
でなくなってしまうケースもあるのです。それは可変長引数が絡んできた場合です。
null
がnull
でなくなってしまう日
まずサンプルプログラムとして、挨拶する文字列を生成するgreetingMessageメソッドがあるとします。みんなの名前を呼んで挨拶したいので、名前は可変長引数として取るとしましょうか。しょうもないメソッドですが、まあサンプルなので…
こんな感じでしょうか。(「さん」が2回出てくるのが微妙ですが…)
public static String greetingMessage(String... names) {
return String.join("さん、", names) + "さんこんにちは!";
}
しかし、これだと引数がない場合は無言になってしまいますし、引数としてnull
が渡された時に若干わかりづらいエラーになってしまいます。そういうわけで、関数の先頭でnull
チェックや要素数のチェックをして、引っかかったら名前なしで挨拶することとしましょうか。
こんな感じですね。(「こんにちは!」が2回(ry)
public static String greetingMessage(String... names) {
if(names == null || names.length == 0) {
return "こんにちは!";
}
return String.join("さん、", names) + "さんこんにちは!";
}
早速試してみましょう。
System.out.println(greetingMessage("太郎"));
System.out.println(greetingMessage("太郎", "花子"));
System.out.println(greetingMessage());
System.out.println(greetingMessage(null));
実行結果
太郎さんこんにちは!
太郎さん、花子さんこんにちは!
こんにちは!
こんにちは!
いい感じです。しかし、ここで若干の問題があります。IntelliJ IDEAを使っている場合、以下のような警告が出ます。
よくわからないですが、なんか曖昧だみたいなことを言われているみたいです。IntelliJ IDEAではAlt + Enterで(可能なら)自動解決してくれますのでやってみましょう。
今まで見てきたようにキャストすれば解決するようですが、選択肢が2つ出ました。どちらを選べばいいのでしょうか。とりあえず試しに上を選んでみましょうか。
黄ばみがとれました。もとい、警告が消えました。では実行してみましょう。引数はnull
ですから、「こんにちは!」と出るはずです。
System.out.println(greetingMessage((String) null));
nullさんこんにちは!
!?
なんか実行結果が変わりましたね…
こういう時はデバッグです。一体何が起こったのか見てみましょう。
どうやらnames
はnull
ではなく、null
を要素として持つ長さ1のString
配列になっているようです。なんと、引数としてnull
を渡したはずなのに、null
だったはずの値がキャストによりString
配列になってしまいました。
null「俺が俺で無くなってゆく」
これが「タイトル回収」ってやつですね?
どうしてこうなった
可変長引数の場合、上で見たように単一の引数を渡すこともできますが、実は配列を渡すことも可能です。
greetingMessage(new String[]{"太郎", "花子"});
しかし、null
の場合は一体そのどちらとして解釈されるのか、プログラマにはわかりづらいですね。IntelliJが出した警告は実はそういう意味でした。
どうやらキャストがない場合は「配列自体がnull
」と解釈されるようで、仮引数のnames
はnull
となります。
こういうイメージです。
String[] names = null;
しかし、可変長引数の単一の要素の型(この例だとString
型)にキャストした場合は「単一の値がnull
」と解釈されるようになります。names
自体はnull
ではなく配列オブジェクトで、要素がnull
ということです。
こういうイメージです。
String[] names = new String[1];
names[0] = null;
そういうわけで、今回の例でもString
にキャストしたことによってnull
が単一の値として解釈されたため、names
自体はnull
ではなく配列となり、引数として渡したnull
はnames
の要素のうちの1つとして収まったわけですね。(まあ要素は1つしかないけど)
これで何が起こったのかわかりました。すっきりしましたね。
…あれ?だとしたらnull
がキャストによって別の値に変換されたわけじゃなくね?タイトル詐欺じゃね?って書いてる途中に気づいたのですが、もうここまで書いちゃったしもう時間ないしこのまま投稿してしまいますすいません許してくださいなんでも(ry
想定通り警告を消すには?
気を取り直して…メインテーマは以上なのですが、ただ警告を消したかっただけなのにスネ夫化してしまったのは…じゃなかった、実行結果が変わってしまったのはうれしくありません。どうすれば実行結果を変えずに警告を消して幸せになれるでしょうか?
といっても特に難しいことはなく、配列でない型にキャストするからこうなったわけなので、配列型にキャストすればいいのです。すなわちIntelliJが提示してくれた選択肢の、選ばなかったほうの2つ目です。
実行してみましょう。
System.out.println(greetingMessage((String[]) null));
こんにちは!
最高や!「nullさん」なんて最初からいなかったんや!
これで警告なしで想定通りの結果になり、挨拶するたび友達増えてみんな幸せです。
まとめ
-
null
は型が曖昧だが、キャストによって何の型なのかコンパイラに教えることができ、それによりコンパイルエラーや警告が消える - 可変長引数に対して
null
リテラルを渡す際、単一の型にキャストするとnull
がnull
でなくなってしまう(ように見える) - 内容もだいぶふざけてました、ごめんなさい
余談
引数として複数のnull
を渡した場合は今回の警告は発生しません。その場合は「単一の値が複数」としか解釈できないためです。
そのため、この場合は黄ばみません。
余談その2
今回の事例は私が実際に見たことが元になっています。つまり「可変長引数を取り、かつその仮引数に対してnull
チェックを行うメソッド」のユニットテストでnull
リテラルを渡しており、それによってIntelliJに怒られ、お怒りを鎮めるためにAlt + Enterで最初に出た解決策をとりあえず選んでコミット、プッシュしたことでテストが壊れた…ということが現実に起こったのです。
こんな記事ですが、IntelliJのお導きを無批判に受け入れてはならない、という教訓は汲み取れるかもしれません。一応。