Kotlin Null安全性(Null Safety)
背景
java.lang.NullPointerExceptionは、Java開発者がよく遭遇する例外としてなじみ深いものだろう。オブジェクトを参照しようとしたとき、そのオブジェクトがnullである場合に投げられる例外である。具体的には、String型の変数にnullを代入しておき、その変数に対してlengthメソッドを呼び出した場合にNullPointerException(以下、NPE)が発生する。
nullは値がないときに使用される。たとえば、指定されたIDを持つユーザーが存在しないとき、findUserByIdのようなメソッドはUserクラスのインスタンスを返す代わりにnullを返すことがある。
この観点では、nullは便利に機能すると言える。しかし、このnullによって開発者は見たくない例外であるNPEに遭遇する。適切にnullチェックを行えば、具体的にはif条件文でnullではないことを確認すれば回避できるのに、なぜそれができないのだろうか。
nullを返さないと分かっているメソッドの戻り値に対しては、nullチェックをしないのが一般的だろう。ここで重要なのは、仕様上nullを返さないはずのメソッドがあっても、Javaのコードではそれを保証できないという点である。Javaでは、変数やメソッドの戻り値はいつどこでもnullになり得る。つまり、nullチェックが必要なものと不要なものが混在しているため、誤ってNPEを発生させる可能性がある。
Javaにおけるnullへの対処方法
nullかもしれないものとnullではないものを区別する方法はいくつかある。
メソッドシグネチャで知らせる
原始的な方法である。nullを返す可能性があるメソッドのシグネチャで、開発者に注意を促す。たとえばgetNameOrNullのような名前のメソッドである。メソッド名を見れば、nullが返るかもしれないと気付ける。
静的解析ツール
メソッドにアノテーションを付け、静的解析ツールで指摘する方法である。getNameメソッドがnullを返す可能性がある場合は@Nullable String getName() {...}のように書き、nullを返さない場合は@NotNull String getName() {...}と書く。
型で表現する
存在しない可能性がある値を表すために、nullの代わりに新しく定義された型を使う方法である。具体的には、Java SE 8で導入されたjava.util.Optionalクラスである。値がなければOptional#emptyで返されるオブジェクトを使い、値が存在すればその値をOptional#ofの引数に渡してラップする。Optional型は、存在しない可能性がある値と必ず存在する値を区別するための、便利で分かりやすいメソッドを多数提供している。
Kotlinのnull安全性
静的解析ツールやOptionalを使うことはとても良い方法である。しかし、Javaでは依然として変数やメソッドの戻り値がいつどこでもnullになり得る。次の例は@NonNullとOptional型を使っているにもかかわらず、NPEが発生する。
例: nullは依然として発生する
import org.springframework.lang.NonNull;
import java.util.Optional;
public class Foo {
// Javaである。
@NonNull
Optional<String> getName() {
return null;
}
public static void main(String[] args) {
Foo foo = new Foo();
foo.getName().toString();
}
}
Output:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.toString()" because the return value of "Foo.getName()" is null
at Foo.main(Foo.java:15)
そこでKotlinのnull安全性機能が登場した。Kotlinでは、nullになり得る値(以下Nullable)とnullになり得ない値(以下NotNull)の区別を、言語組み込みの機能として提供する。
Optionalを使う方法とは異なり、新しいインスタンスの生成やGCが不要なため、その分のオーバーヘッドがなく、Androidなどリソースが限られた環境で有利だと考えられる。
基本的な使い方
前に、変数を初期化して再代入することについて説明し、使用した。変数aはString型である。ここでは型を明示しているが、省略しても問題ない。varキーワードによって変更可能な変数として宣言されているため、"Goodbye"を再代入できる。しかし、その次の行でnullを代入する部分ではコンパイルエラーが発生する。変数aはNotNullとして宣言されているため、nullの代入をコンパイラが許可しない。これは、変数aは常にnullではないと安心して使えることを意味する。
例: NotNull変数
var a: String = "Hello"
a = "Goodbye"
a = null // ここでコンパイルエラーが発生する。
では、nullを代入できるNullable変数はどのように宣言すればよいだろうか。簡単である。単に型の後ろに?を付けるだけでよい。次の例を見ると、変数bの型がString?になっている。これは「nullを代入できるString型」を意味する。2行目でnullを代入しているが、コンパイルできる。この場合、変数bの型から疑問符(?)を省略できない。省略すると、"Hello"はString?ではなくStringとして推論されるためである。
例: Nullable変数
var b: String? = "Hello"
b = null
KotlinはNullableとNotNullを明確に区別する。NotNullの変数にはnullが入らないため、それを参照するときにNPEが起きず安全である。では、Nullable変数にはnullが入るためNPEが発生するのではないだろうか。そこで、次のように意図的にNPEを起こしてみよう。
例: NPEを発生させてみる
val str: String? = null
str.length // ここでコンパイルエラーが発生する。
String?変数であるstrをnullで初期化し、このstr変数にlengthプロパティを呼び出してNPEを起こそうとしている。しかし実際にはコンパイル時にエラーが発生する。KotlinはNPEを起こさせたくないため、NPEの可能性がある操作をコンパイル時にエラーにする。Nullableに対するメソッドやプロパティへの参照は禁止されている。
しかし現実問題として、Nullableのメンバーに常にアクセスできないのであれば、決して使いものにならない。もちろんアクセスする方法がないわけではない。その方法はnullをチェックすることである。
次のように変数strがnullかどうかを確認すると、保証される範囲(ifブロック内)でstrをNotNullとして扱える。strに"Hello"が代入されている状態で実行すると、"5"が出力される。
例: nullをチェックするとNotNullになる
if(str != null) {
println(str.length)
}
Nullableの便利な機能
KotlinのNullableを使うために必要な知識は、ここまでの内容で十分かもしれない。しかし、nullチェックを書くのは退屈で面倒な作業なので、KotlinはNullableを便利に扱うための機能を提供している。
安全呼び出し(Safe Call)
前では、Nullableのメソッドを呼び出すためにnullチェックを行った。これを簡潔に書ける安全呼び出し(Safe Call)というものがある。
次の例では、String?型の変数strのlengthプロパティを安全に呼び出している。通常のメソッド呼び出しと異なる点は、ドット(.)の前に疑問符(?)を入れることである。こうすると安全呼び出しになり、Nullableのメソッドやプロパティを安全に、つまりNPEを発生させずに呼び出せる。
例: 安全呼び出し
val str: String? = null
// 安全呼び出し方式
val length1: Int? = str?.length
// nullチェック方式
val length2: Int? = if(str != null) str.length else null
もしstrがnullであれば、メソッドを呼び出さずにnullを返す。1つ目の安全呼び出し方式と2つ目のnullチェック方式は同じである。
安全呼び出しは、メソッドチェーン(Method chaining)として書きたい場合などに特に効果的である。foo()?.bar()?.baz()のように書いた場合、途中でnullになっても安全呼び出しがチェーンされ、最終的にnullが返るだけである。
例: 安全呼び出しでのメソッドチェーン
// 安全呼び出し方式
val result1 = foo()?.bar()?.baz()
// nullチェック方式
val foo = foo()
val result2 = if(foo != null) {
val bar = foo.bar()
if(bar != null) bar.baz() else null
} else {
null
}
デフォルト値
元の値がnullだったときに使う値を簡単に指定できる。Nullableの後ろにエルビス演算子(?:)とデフォルト値を書く。次の例を見てみよう。s?.lengthは安全呼び出しによって文字列の長さまたはnullを返す。nullが返された場合は、デフォルト値として0になるように指定している。nullではなければ、メソッド呼び出しで返された値が代入される。
例: デフォルト値
val s: String? = "Hello"
// エルビス演算子を使用
val length1: Int = s?.length ?: 0
// nullチェック方式
val length: Int? = s?.length
val length2: Int = if(length != null) length else 0
nullではないことを強調する!!演算子(not-null assertion operator)
最後に紹介するのは!!演算子である。
!!演算子はNullable変数を強制的にNotNullに変換する。次の例では、String?型の変数strを!!演算子によって強制的にStringへ変換している。
例: 強制的にNotNullへ変換
val str: String? = "Hello"
println(str!!.length) // => 5
この例は問題なく実行された。しかし、次の例ではランタイム例外が発生する。nullの場合に!!演算子を使うと、NullPointerExceptionが発生する。
例: ランタイム例外の発生
val str: String? = null
println(str!!.length)
nullの場合には例外が発生する。これは結局、これまでと同じである。!!演算子を使いたい場合は、まずnullチェック、安全呼び出し、デフォルト値の使用を検討すべきである。どうしても必要な場合だけ!!演算子を使うようにしよう。また、その理由や経緯をコメントとして残しておくのもよい。
さらに、!!演算子を使いたくなる別の例を見てみよう。要素にnullを許可する次の例で、nullを取り除き、NotNull要素だけの新しいリスト(List<T>)を受け取りたい関数が必要なら、次のように実装できる。
例: filterNotNullの実装
fun <T> filterNotNull(list: List<T?>): List<T> =
list.filter { it != null }
.map { it!! }
fun main() {
val a: List<String?> = listOf("kimkc", null, "devkuma")
val b: List<String> = filterNotNull(a)
println(b) // => ["kimkc", "devkuma"]
}
list.filter { it != null }によって、要素がnullではない場合だけフィルタリングされる。しかしリストの型は依然としてList<T?>のままである。そのため、次にmap { it!! }で要素の型をT?からTへ強制的に変換する。
実際には!!演算子を使わずに実装でき、次のようにfilterNotNullはコレクションの標準メソッドとして提供されている。
例: filterNotNullコレクション標準メソッドの使用
val a: List<String?> = listOf("kimkc", null, "devkuma")
val b: List<String> = a.filterNotNull()
println(b) // => ["kimkc", "devkuma"]
標準拡張関数 let
Kotlinの標準ライブラリは、すべての型に対してletという拡張関数を提供している。正確にはpublic inline指定されているが、便宜上ここでは省略する。
コード: 標準拡張関数let
fun <T, R> T.let(f: (T) -> R): R = f(this)
letはレシーバー(receiver)となるオブジェクト(this)に、引数として渡された関数(f)を適用している。使用例は次のとおりである。
例: letの使用例
5.let {
println(it * 3) // => 15
}
関数リテラルに渡される唯一の引数(暗黙変数it)は、letのレシーバーと同じオブジェクトなので、この場合itには5が入る。
では、この単純な関数は何の役に立つのだろうか。Nullableに対して、NotNull引数を扱う関数を適用するときに便利である。具体的に見てみよう。
次の例では、NotNullのIntを引数に取るsuccess関数を定義している。Nullable変数aをこの関数の引数として渡したいが、NullableとNotNullの違いがあるため渡せない。successは関数であり、Intのメソッド(または拡張関数)ではないため、安全呼び出しも使えない。この場合、if文でnullチェックを行い、Nullableを安全にNotNullとして渡すしかない。
例: NotNullを受け取る関数を適用したい
fun success(n: Int): Int = n + 1
// Nullableな変数
val a: Int? = 3
val b: Int? =
if (a != null) success(a)
else null
println(b) // => 4
このような場合にletを使えばよい。letはすべての型の拡張関数なので、a?.let {...}のように安全呼び出しできる。そしてletの引数となる関数(「コード: 標準拡張関数let」のfに該当)が受け取る引数は、レシーバーと同じ型のNotNullである。レシーバーがnullのときはlet拡張関数が呼び出されないため、これに適している。
そこでletを使うように変更すると、successを適用する部分を次のように書き換えられる。
例: letを使うとすっきりする
val b: Int? = a?.let { success(it) }
これをさらに簡潔に、次のように書くこともできる。
例: 関数参照でさらにすっきり
val b: Int? = a?.let(::success)
Javaで既にOptionalに慣れている開発者にとっては、letがOptionalのmap、flatMap、ifPresentの3つの役割を担うものだと考えると理解しやすいだろう。