読者です 読者をやめる 読者になる 読者になる

Kotlin Advent Calendar 2012 (全部俺)

JavaプログラマのためのKotlin入門

16日目:ぬるぽとの別れ

f:id:ngsw_taro:20121215144401j:plain

今日は、面倒な奴だけど憎めない、そんなNullPointerExceptionとの別れの日です。KotlinにはNPEを起こさない仕組みが備わっています。それが今日取り上げる機能、NULL安全です。

nullをセットできない型

Javaでは、すべての参照型変数がnullとなることができるので、NPEが発生するリスクを常に持っています。Kotlinの型システムは、nullという状態を取り得る型と、取り得ない型を明確に区別します。これがNULL安全を実現する仕組みです。前者をNULL許容型、後者を非NULL型と言います。

コード上では、非NULL型は通常の型宣言で表し、NULL許容型は型名の後に?を付けることで表現します。次のコードは非NULL型の変数にnullをセットしようしていますがコンパイルエラーとなります。

val str : String = null

熟考の末、この変数がnullを取り得るべきであると感じたなら、型名の後に?を付けて、コンパイラにNULL許容型であることを伝えます。次のコードは上手くコンパイルできます。

val str : String? = null

NULL許容型へのアクセスは慎重に

非NULL型の変数は絶対にnullではないということが保証されているので安心ですね。しかし、NULL許容型の変数はnullを取り得るので、そこにアクセスを試みればNPEが起こるのではないかと心配になります。安心してください。NULL許容型の変数には簡単にはアクセスできないようになっています。次のコードはNULL許容型の文字列 str の文字数を取得しようとしています。str には適切に文字列が設定されているので null ではありませんが、このコードはコンパイルに失敗します。

val str : String? = "Advent Calendar"
val length = str.length

では、どうやってNULL許容型の変数にアクセスすればいいのでしょうか。

その1:NULLチェック

方法はいくつかありますが、まずNULLチェックによる方法が挙げられます。NPEが起こらないようにアクセス対象の変数が null でないかを if を使ってチェックします。

val str : String? = "Advent Calendar"
val length = if(str != null) {
  str.length
} else {
  null
}
  
println(length) // 17

if で str が null でないことを確認していることをコンパイラは知っているので、そのブロック内でのみ str が指すオブジェクトにアクセスできます。ただし str が今回の場合のように val 宣言されていることが前提です。var 宣言の変数は常に null になる機会をもっているため、この方法ではアクセスできません。

その2:安全呼び出し

2つ目の方法は安全呼び出しによるものです。通常、変数を介してオブジェクトのメンバにアクセスするためには*1変数名とメンバ名の間にドットを置きますが、安全呼び出しでは、ドットの代わりに?.を使用します。something?.do()という具合に。安全呼び出しは、var宣言されている変数でも使用可能です。

var str : String? = "Advent Calendar"
val length = str?.length

もし、null 状態の変数に対して安全呼び出しを行うとどうなるのでしょうか。そんな場合でもNPEは発生しません。単に null を返すだけです。

var str : String? = null
val length = str?.length
  
println(length == null) // true

これは便利です。安全呼び出しチェーンを形成した場合でも、途中で失敗してプログラムがクラッシュするという問題はありません。結局、nullが返されるだけです。

val str : String? = null
val length = str?.trim()?.length
  
println(length == null) // true

その3:演算子 !!

最後に挙げる、この方法はぬるぽフェチのための方法です。NULL許容型の変数の後に演算子 !! を記述すると、その変数の非NULL型バージョンを返します。しかし、変数が null である場合、NPEを投げます

val hoge : String? = "hoge"
println(hoge!!.toUpperCase()) // HOGE
  
val fuga : String? = null
println(fuga!!.toUpperCase()) // throw NPE!!!

変数 fuga に演算子 !! を適用した瞬間にNPEがスローされプログラムはクラッシュします。3つ目のこの方法を検討するときは注意が必要です。

おまけ:エルビス演算子

NULL許容型について次のようなコードをよく書くことがあるかも知れません。

val r = if(hoge != null) hoge.toString()
        else "hoge is null."

つまり、変数が null でない場合は関数を呼び出してその結果を返すが、null である場合はそのとき専用の値を返すような場合です。この場合にはエルビス演算子が便利です。?: がその演算子です。上記のコードをエルビス演算子を用いて記述し直すと次のようになります。

val r = hoge?.toString() ?: "hoge is null."

エルビス演算子の左辺が null でなければ、そのままその値を返しますが、null である場合に限り右辺を返す、という演算子です。

名前の由来は横から見ると、かのロック歌手のエルビス・プレスリーに見えるからです。

おまけ:Kotlinにおける同値比較

2種類の同値比較があります。両オブジェクトが値として等しいかどうかを調べる比較と、両ポインタが同一のオブジェクトを指しているかどうかを調べる比較です。

前者の方法で比較するには演算子 == を使用します(Javaとは違うことに注意してください)。後者の方法で比較するには関数 identityEquals を使用します。

val a : Int = 128
val b : Int = 128
  
println(a == b) // true
println(a identityEquals b) // false

変数 a と b には同じ数値を代入しています。そのため == による比較では true が返されますが、オブジェクト的には別物なので identityEquals による比較では false が返されています。

おまけのおまけ

余談ですが、上記の例で128を選んだのには理由があります。いや、128以外でもいいのですが、-128から127の範囲内だと都合が悪い理由がありました。Int型の値は基本的にjava.lang.Integerの値です。メソッド Integer.valueOf によって整数のインスタンスを取得します。java.lang.Integerでは、-128から127のIntegerインスタンスプールしており、オブジェクトを使い回しています(インスタンス生成のコストを避けるため)。そのため、上記の例で-128から127の範囲内の値で実行してみると identityEquals で比較した際にも true を返します。

さらにもっとおまけですが、Kotlinコンパイラは非NULL型Intの場合にはIntegerを使用せず、プリミティブ型の int を使用します。オブジェクトが必要な場合には、必要になった場所で Integer.valueOf を使ってオブジェクトを取得します。

以上を踏まえて、面白い実験をします。次のコードの結果を予想できますか?

fun main(args : Array) {
  val i : Int = 128
  val j : Int = i
  println(i identityEquals j)

  val a : Int? = 128
  val b : Int? = a
  println(a identityEquals b)
}

なんと1回目の比較では false が、2回目の比較では true が返されます。変数 i と j はプリミティブ型の int としてコンパイルされ128がそれぞれに代入されます。identityEquals により比較されるタイミングでそれらの変数を参照型に変換します。そこで Integer.valueOf が使用されます。128という数値はオブジェクトプールに存在しないので、毎回インスタンスを生成することになります。したがって、identityEquals による比較では別物という判断が下ります。

変数 a と b はNULL許容型なのでint型ではなく Integer型としてコンパイルされます。a に128を代入しようとしているのでInteger.valueOf(128) によりインスタンスが生成されます。そして、その参照を b にコピーしています。ということは、当然ながら identityEquals による比較で同一と見なされます。

コンパイラのバージョンが上がれば、このような挙動はなくなるのかな?

まとめと次回予告

今日はNULL安全という仕組みを見てきました。nullという状態を取り得る型とそうでない型があることと、それぞれをNULL許容型、非NULL型と呼ぶことを学びました。NULL許容型変数にアクセスする際には、特別なやり方が必要です。NULLチェックを突破したり、安全呼び出しを行ったり、もしくはちょっと危険な演算子 !! を使用したりします。最後に挙げた方法を除けば、これらはNullPointerExceptionの恐怖から我々プログラマを解放してくれます。そして、おまけとしてエルビス演算子と、Kotlinにおける同値比較について学びました。

明日もコードを安全に保ってくれる素晴らしい機能についてお話しします。型の安全を保証しつつ、柔軟に扱えるジェネリクスの登場です。お楽しみに!

日記

投票行きます。

*1:ちょっと回りくどい言い方に聞こえるかも知れません。今日はは変数にフォーカスして話をしているからです。nullという状態を持ち得るのは「変数」であってオブジェクトではありません。変数はオブジェクトへのポインタを格納していることを考えればしっくりきますね。