5.4 NULL安全

nullを代入できない型

 誰もが経験するプログラミングの失敗として名高いNullPointerExceptionを駆逐する仕組みをKotlinは提供しています。NullPointerException(以下NPEと記述)、「ぬるぽ」の愛称で親しまれているそれは、プログラマのミスで混入してしまいます[1]。まぁ、ミスなんですが、言語レベルでそのようなミスをなくそうというのがNULL安全のモチベーションです。
 Javaでは、すべての参照型変数がnullを持つことができるので、NPEが発生するリスクを常に持っています。Kotlinの型システムは、nullを保持し得る型と、保持し得ない型を明確に区別します。これがNULL安全を実現する仕組みの1つです。前者をNULL許容型(nullable type)、後者を非NULL型(non-null type)と言います。
 コード上では、非NULL型は通常の型宣言で表し、NULL許容型は型名の後に?を付けることで表現します。コード5.4.1に例を示します。

コード5.4.1 非NULL型はnullを格納できない

val a : String = null // NG
val b : String? = null // OK
  
var c = "Kotlin!"
var d : String? = "Kotlin!"
  
c = null // NG
d = null // OK

 変数 aString型として宣言されているので、それにnullを代入することは許されません。一方、変数 bString?型として宣言されています。これはString型でかつ、NULL許容型であることを意味しています。したがって、変数 bnullを代入できます。

考えなしではNULL許容型にアクセスできない

 参照型はNULL許容型と非NULL型の2種類があることを学びました。が、非NULL型を導入すると何が嬉しいのでしょうか。それは、非NULL型にアクセスしてもNPEが発生しないことが保証されていることです。非NULL型変数にnullは代入できませんからね。
 では逆にNULL許容型にアクセスするときには、NPEが発生し得るのでしょうか。答えは「いいえ」です。通常の方法ではNULL許容型にアクセスできないからです。正確に言うと、KotlinはNPEを排除するため、NULL許容型に通常の方法でアクセスできないようにしています。例えばコード5.4.2は、String?型のプロパティ sizeにアクセスしようとしていますが、コンパイラはそれを許しません。

コード5.4.2 NPEの可能性がある呼び出しはできない

val text : String? = "12345"
val size = text.size // NG

 NULL許容型のメンバにアクセスするために、3つの方法が提供されています。1つ目の方法が、条件によるNULLチェック。2つ目の方法が、安全呼び出し。3つ目の方法が!!演算子です。

条件によるNULLチェック

 次に示すコード5.4.3は安全にNULL許容型のメンバにアクセスできます。

コード5.4.3 条件によるNULLチェック

val text : String? = "12345"

if (text != null) {
    println(text.size)
}

 このコードを実行すると期待通り「5」が出力されます。このように、NULLチェックを行ったブロック内では非NULL型のように扱うことができます。そこでNPEが発生しないことをコンパイラは知っているからです。
 ただしこの方法は、イミュータブルな変数(つまりval宣言された変数)のみで使用可能です。NULLチェックを通過した後に、nullが再代入される可能性があるからです。

安全呼び出し

 2つ目の方法は安全呼び出し(safe call)です。通常、メンバにアクセスするためには変数名とメンバ名の間にドットを置きますが、安全呼び出しでは、ドットの代わりに?.を使用します。something?.do()という具合に。
 安全呼び出しによるアクセスはNPEを発生させません。コード5.4.4を見てください。

コード5.4.4 安全呼び出し

val a : String? = "hoge"
println(a?.size) // 4

val b : String? = null
println(b?.size) // null
println(b?.size == null) // true

 変数 bにはnullが格納されています。b.sizeはコンパイルエラーとなりますが、b?.sizeは問題ありません。b?.sizeは文字数を返してくれませんが、nullを返してくれます。
 安全呼び出しを行い、nullが返ってくるということは、失敗を伝搬させることが可能であるということです。これは、呼び出しチェーンにおいて便利です。

!!演算子

 3つ目の方法は!!演算子です。NULL許容型の変数の後に!!演算子を付けると、その変数の値を非NULL型に変換して返します。ただし、変数の値がnullの場合はNPEをスローします!!使い方の例をコード5.4.5に示します。

コード5.4.5 !!演算子

val foo : String? = "1234"
val bar : String = foo!!
println(bar.size) // 4
println(foo!!.size) // 4

val baz : String? = null
println(baz!!.size) // throw NPE

 ちなみに!!演算子は、組み込み関数ではありません。Kotlinの標準ライブラリが提供する機能です。基本的にNPEが発生することがないKotlinですが、明示的にNPEをスローするコードを書いたりすると、もちろんNPEとしてプログラムがクラッシュします。

[1] 非チェック例外がスローされる原因の多くは、プログラミングミスですが...