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

# はじめに
この記事は<a href="https://qiita.com/advent-calendar/2020/java">Java Advent Calendar 2020</a>の19日目の記事です。タイトルはふざけていますが、内容は普通です。たぶん。きっと。

最近寒いですね。こんな寒い時は暖かいコーヒーに限ります。Javaプログラマはみんなコーヒーが好きです[要出典]。というわけで皆さんもコーヒーがお好きだと思いますが、どんなコーヒーがお好きですか？私はここのところはセブンイレブンのグアテマラブレンドを毎日飲んでいるのですが。

さて、突然ですがJavaにおいて`null`をキャストすると何が起こると思いますか？
何も起こらない？そうかもしれないですが、そうでもない場合があります。だいぶ地味な話題ですが、興味があればお付き合いください。

# サンプルコードの実行環境
* macOS Big Sur 11.0.1（20B29）
* Apple M1
* Java 13.0.5.1 (Azul Zulu)

# 基本
まず、最もシンプルなケースについて見てみます。`null`リテラルを何かにキャストするとどうなるでしょうか？

```java
System.out.println((String) null);
System.out.println("done");
```

実行結果

```
null
done
```

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

## なんでそんなことする必要があるんですかね…
そもそも`null`リテラルをキャストしたいことなんてあるのか？という話ではあるんですが、まあ普通はないですね。でも全くないとは限りません。
たとえば、上の例でキャストを外すとどうなるでしょうか。

```java
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`リテラルではどちらとも解釈できてしまうからどっちを呼べばいいかコンパイラには判断できずにエラーとなるわけですね。[^println]
[^println]: まあ`char[]`と`String`だけでなく、もっとたくさんありますけれども。

そこでキャストの出番です。キャストすればどの型なのかはっきりしますので、コンパイラもどのメソッドを呼ぶのか判断でき、コンパイルが通って実行もできるというわけです。

## それ以前の話
いやいや、そもそもサンプルコード以外で`null`リテラルを渡したいことなんてあるのかというツッコミが入るかもしれません。まあ普通はないですね。でも全くないとは限りません。たとえば、引数として`null`が渡された場合に例外を投げるメソッドはよくありますので、それに対するテストコードでは`null`リテラルを渡すことがあり得ます。
それで、前述のようなエラーやその他警告などが出た場合は、解決策の一つとして`null`をキャストするということがあるわけです。[^another_solution]
[^another_solution]: この記事では扱いませんが、他の解決策として`null`リテラルを直接渡すのではなく、いったん変数（引数の型の変数）に代入した上でそれを渡す、というものもあります。

# キャストにより値が変わってしまうケース
さて、上記の例におけるキャストは`null`を何の型として解釈すべきなのかをただコンパイラに教えてあげるだけのもので、キャストによって`null`が何か別の値に変換されるわけではありませんでした。`null`リテラルに限らず、参照型におけるキャストとは一般的にそういうものです。[^cast_primitive]
[^cast_primitive]: プリミティブ型の場合はさておき。

しかし、なんと`null`リテラルをキャストした時に、`null`が`null`でなくなってしまうケースもあるのです。それは可変長引数が絡んできた場合です。

## `null`が`null`でなくなってしまう日
まずサンプルプログラムとして、挨拶する文字列を生成するgreetingMessageメソッドがあるとします。みんなの名前を呼んで挨拶したいので、名前は可変長引数として取るとしましょうか。しょうもないメソッドですが、まあサンプルなので…
こんな感じでしょうか。（「さん」が2回出てくるのが微妙ですが…）

```java
public static String greetingMessage(String... names) {
    return String.join("さん、", names) + "さんこんにちは！";
}
```

しかし、これだと引数がない場合は無言になってしまいますし、引数として`null`が渡された時に若干わかりづらいエラーになってしまいます。そういうわけで、関数の先頭で`null`チェックや要素数のチェックをして、引っかかったら名前なしで挨拶することとしましょうか。
こんな感じですね。（「こんにちは！」が2回（ｒｙ）

```java
public static String greetingMessage(String... names) {
    if(names == null || names.length == 0) {
        return "こんにちは！";
    }
    return String.join("さん、", names) + "さんこんにちは！";
}
```
早速試してみましょう。

```java
System.out.println(greetingMessage("太郎"));
System.out.println(greetingMessage("太郎", "花子"));
System.out.println(greetingMessage());
System.out.println(greetingMessage(null));
```
実行結果

```
太郎さんこんにちは！
太郎さん、花子さんこんにちは！
こんにちは！
こんにちは！
```

いい感じです。しかし、ここで若干の問題があります。IntelliJ IDEAを使っている場合、以下のような警告が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/6fb61d6d-73cb-c7dd-46eb-28924046269d.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/b46b5ac3-7bd0-d6e7-ec54-f9373603e302.png)
今まで見てきたようにキャストすれば解決するようですが、選択肢が２つ出ました。どちらを選べばいいのでしょうか。とりあえず試しに上を選んでみましょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/a0ec2f71-2d8d-d599-5370-74fe0766773c.png)
黄ばみがとれました。もとい、警告が消えました。では実行してみましょう。引数は`null`ですから、「こんにちは！」と出るはずです。

```java
System.out.println(greetingMessage((String) null));
```

```
nullさんこんにちは！
```

！？
なんか実行結果が変わりましたね…
こういう時はデバッグです。一体何が起こったのか見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/5e79dfe2-639b-a770-aa93-c67c348a4ab6.png)
どうやら`names`は`null`ではなく、`null`を要素として持つ長さ1の`String`配列になっているようです。なんと、引数として`null`を渡したはずなのに、`null`だったはずの値がキャストにより`String`配列になってしまいました。

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

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

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

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

しかし、`null`の場合は一体そのどちらとして解釈されるのか、プログラマにはわかりづらいですね。IntelliJが出した警告は実はそういう意味でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/6fb61d6d-73cb-c7dd-46eb-28924046269d.png)

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

```java
String[] names = null;
```


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

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

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

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

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

といっても特に難しいことはなく、配列でない型にキャストするからこうなったわけなので、配列型にキャストすればいいのです。すなわちIntelliJが提示してくれた選択肢の、選ばなかったほうの２つ目です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/06794eed-bb13-32be-4928-43a64904e5fd.png)

下を選ぶとこんな感じ。警告が消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/cfffbd60-ec6f-fe38-21c7-95a71fc2ee69.png)

実行してみましょう。

```java
System.out.println(greetingMessage((String[]) null));
```

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

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

# 余談
引数として複数の`null`を渡した場合は今回の警告は発生しません。その場合は「単一の値が複数」としか解釈できないためです。
そのため、この場合は黄ばみません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321705/06cd8367-0757-e884-12ac-6b749293ec99.png)

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