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になり得る。次の例は@NonNullOptional型を使っているにもかかわらず、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などリソースが限られた環境で有利だと考えられる。

基本的な使い方

前に、変数を初期化して再代入することについて説明し、使用した。変数aString型である。ここでは型を明示しているが、省略しても問題ない。varキーワードによって変更可能な変数として宣言されているため、"Goodbye"を再代入できる。しかし、その次の行でnullを代入する部分ではコンパイルエラーが発生する。変数aNotNullとして宣言されているため、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はNullableNotNullを明確に区別する。NotNullの変数にはnullが入らないため、それを参照するときにNPEが起きず安全である。では、Nullable変数にはnullが入るためNPEが発生するのではないだろうか。そこで、次のように意図的にNPEを起こしてみよう。

例: NPEを発生させてみる

val str: String? = null
str.length // ここでコンパイルエラーが発生する。

String?変数であるstrnullで初期化し、このstr変数にlengthプロパティを呼び出してNPEを起こそうとしている。しかし実際にはコンパイル時にエラーが発生する。KotlinはNPEを起こさせたくないため、NPEの可能性がある操作をコンパイル時にエラーにする。Nullableに対するメソッドやプロパティへの参照は禁止されている。

しかし現実問題として、Nullableのメンバーに常にアクセスできないのであれば、決して使いものにならない。もちろんアクセスする方法がないわけではない。その方法はnullをチェックすることである。

次のように変数strnullかどうかを確認すると、保証される範囲(ifブロック内)でstrNotNullとして扱える。str"Hello"が代入されている状態で実行すると、"5"が出力される。

例: nullをチェックするとNotNullになる

if(str != null) {
  println(str.length)
}

Nullableの便利な機能

KotlinのNullableを使うために必要な知識は、ここまでの内容で十分かもしれない。しかし、nullチェックを書くのは退屈で面倒な作業なので、KotlinはNullableを便利に扱うための機能を提供している。

安全呼び出し(Safe Call)

前では、Nullableのメソッドを呼び出すためにnullチェックを行った。これを簡潔に書ける安全呼び出し(Safe Call)というものがある。

次の例では、String?型の変数strlengthプロパティを安全に呼び出している。通常のメソッド呼び出しと異なる点は、ドット(.)の前に疑問符(?)を入れることである。こうすると安全呼び出しになり、Nullableのメソッドやプロパティを安全に、つまりNPEを発生させずに呼び出せる。

例: 安全呼び出し

val str: String? =  null

// 安全呼び出し方式
val length1: Int? = str?.length

// nullチェック方式
val length2: Int? = if(str != null) str.length else null

もしstrnullであれば、メソッドを呼び出さずに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に慣れている開発者にとっては、letOptionalmapflatMapifPresentの3つの役割を担うものだと考えると理解しやすいだろう。

参考