12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaAdvent Calendar 2020

Day 19

null「俺が俺で無くなってゆく」

Last updated at Posted at 2020-12-18

はじめに

この記事は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

何のエラーもなく実行されました。キャストしただけではNullPointerExecptionClassCastExeptionも発生しません。

なんでそんなことする必要があるんですかね…

そもそも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リテラルをキャストした時に、nullnullでなくなってしまうケースもあるのです。それは可変長引数が絡んできた場合です。

nullnullでなくなってしまう日

まずサンプルプログラムとして、挨拶する文字列を生成する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を使っている場合、以下のような警告が出ます。
image.png

よくわからないですが、なんか曖昧だみたいなことを言われているみたいです。IntelliJ IDEAではAlt + Enterで(可能なら)自動解決してくれますのでやってみましょう。

image.png
今まで見てきたようにキャストすれば解決するようですが、選択肢が2つ出ました。どちらを選べばいいのでしょうか。とりあえず試しに上を選んでみましょうか。

image.png
黄ばみがとれました。もとい、警告が消えました。では実行してみましょう。引数はnullですから、「こんにちは!」と出るはずです。

System.out.println(greetingMessage((String) null));
nullさんこんにちは!

!?
なんか実行結果が変わりましたね…
こういう時はデバッグです。一体何が起こったのか見てみましょう。
image.png
どうやらnamesnullではなく、nullを要素として持つ長さ1のString配列になっているようです。なんと、引数としてnullを渡したはずなのに、nullだったはずの値がキャストによりString配列になってしまいました。

null「俺が俺で無くなってゆく」

これが「タイトル回収」ってやつですね?

どうしてこうなった

可変長引数の場合、上で見たように単一の引数を渡すこともできますが、実は配列を渡すことも可能です。

greetingMessage(new String[]{"太郎", "花子"});

しかし、nullの場合は一体そのどちらとして解釈されるのか、プログラマにはわかりづらいですね。IntelliJが出した警告は実はそういう意味でした。
image.png

どうやらキャストがない場合は「配列自体がnull」と解釈されるようで、仮引数のnamesnullとなります。
こういうイメージです。

String[] names = null;

しかし、可変長引数の単一の要素の型(この例だとString型)にキャストした場合は「単一の値がnull」と解釈されるようになります。names自体はnullではなく配列オブジェクトで、要素がnullということです。
こういうイメージです。

String[] names = new String[1];
names[0] = null;

そういうわけで、今回の例でもStringにキャストしたことによってnullが単一の値として解釈されたため、names自体はnullではなく配列となり、引数として渡したnullnamesの要素のうちの1つとして収まったわけですね。(まあ要素は1つしかないけど)
これで何が起こったのかわかりました。すっきりしましたね。

…あれ?だとしたらnullがキャストによって別の値に変換されたわけじゃなくね?タイトル詐欺じゃね?って書いてる途中に気づいたのですが、もうここまで書いちゃったしもう時間ないしこのまま投稿してしまいますすいません許してくださいなんでも(ry

想定通り警告を消すには?

気を取り直して…メインテーマは以上なのですが、ただ警告を消したかっただけなのにスネ夫化してしまったのは…じゃなかった、実行結果が変わってしまったのはうれしくありません。どうすれば実行結果を変えずに警告を消して幸せになれるでしょうか?

といっても特に難しいことはなく、配列でない型にキャストするからこうなったわけなので、配列型にキャストすればいいのです。すなわちIntelliJが提示してくれた選択肢の、選ばなかったほうの2つ目です。
image.png

下を選ぶとこんな感じ。警告が消えます。
image.png

実行してみましょう。

System.out.println(greetingMessage((String[]) null));
こんにちは!

最高や!「nullさん」なんて最初からいなかったんや!
これで警告なしで想定通りの結果になり、挨拶するたび友達増えてみんな幸せです。

まとめ

  • nullは型が曖昧だが、キャストによって何の型なのかコンパイラに教えることができ、それによりコンパイルエラーや警告が消える
  • 可変長引数に対してnullリテラルを渡す際、単一の型にキャストするとnullnullでなくなってしまう(ように見える)
  • 内容もだいぶふざけてました、ごめんなさい

余談

引数として複数のnullを渡した場合は今回の警告は発生しません。その場合は「単一の値が複数」としか解釈できないためです。
そのため、この場合は黄ばみません。
image.png

余談その2

今回の事例は私が実際に見たことが元になっています。つまり「可変長引数を取り、かつその仮引数に対してnullチェックを行うメソッド」のユニットテストでnullリテラルを渡しており、それによってIntelliJに怒られ、お怒りを鎮めるためにAlt + Enterで最初に出た解決策をとりあえず選んでコミット、プッシュしたことでテストが壊れた…ということが現実に起こったのです。
こんな記事ですが、IntelliJのお導きを無批判に受け入れてはならない、という教訓は汲み取れるかもしれません。一応。

  1. まあchar[]Stringだけでなく、もっとたくさんありますけれども。

  2. この記事では扱いませんが、他の解決策としてnullリテラルを直接渡すのではなく、いったん変数(引数の型の変数)に代入した上でそれを渡す、というものもあります。

  3. プリミティブ型の場合はさておき。

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?