Kotlinのnull安全|?・!!・エルビス演算子の使い分けを解説

Kotlinを学び始めると、型の後ろに付く?や、見慣れない!!、?:といった記号に出会います。最初は煩雑に見えますが、これらはすべてnull安全という1つの仕組みのための道具です。

この記事では、Kotlin公式ドキュメントに沿って、それぞれの意味と使い分けを解説します。

null安全は何のためにあるのか

公式ドキュメントには、null安全について次のように書かれています。

null安全は、10億ドルの過ち(The Billion-Dollar Mistake)としても知られるnull参照のリスクを大幅に減らすために設計されたKotlinの機能である

公式によれば、Javaを含む多くの言語で最もよくある落とし穴が「nullの参照に対してメンバーにアクセスすると、null参照の例外が発生する」ことです。JavaでいうNullPointerExceptionです。

Kotlinはこれに対し、nullを許すかどうかを型システムの一部として明示的に扱います。公式の説明では、null不可の変数を宣言すると、コンパイラがその変数にnullを保持させないよう強制します。

要点は、公式が書いているとおり「実行時ではなくコンパイル時に、nullに関する潜在的な問題を捕まえる」ことにあります。

Kotlinでそれでも例外が起きる場合

公式は、KotlinでNullPointerExceptionが起こりうる原因を明示的に列挙しています。主なものは次のとおりです。

  • 明示的に例外を投げた場合
  • 非null表明演算子!!を使った場合
  • 初期化中のデータ不整合
  • Javaとの相互運用に起因する問題

つまり、自分から!!を書かない限り、Kotlinのコードでこの例外に遭遇する場面はかなり限られます。この事実は、後述する!!の扱い方に直結します。

型の後ろの疑問符

基本の区別から確認します。公式によれば、通常のString型の変数はnullを保持できません。

var a: String = "abc"
a = null   // エラー:Null can not be a value of a non-null type String

nullを許すには、型の直後に?を付けます。

var b: String? = "abc"
b = null   // これは通る

ただし、null許容の変数にそのままプロパティへアクセスするとコンパイラが止めます。

var b: String? = null
val l = b.length   // エラー
// Only safe (?.) or non-null asserted (!!.) calls are allowed
// on a nullable receiver of type String?

このエラーメッセージ自体が、次に使うべき道具を示しています。

道具1:ifによるnullチェック

最も素直な方法です。

val b: String? = null
val l = if (b != null) b.length else -1

ここで重要なのが、公式が説明しているスマートキャストです。コンパイラはnullでないと確認した情報を追跡し、null許容のString?からnull不可のStringへ型を変えます。そのためifの中ではb.lengthをそのまま書けます。

道具2:安全呼び出し演算子

公式の説明では、安全呼び出し演算子?.は「例外を投げる代わりに、対象がnullなら単にnullを返す」ものです。

val a: String? = "Kotlin"
val b: String? = null

println(a?.length)   // 6
println(b?.length)   // null

この演算子が最も効くのは、連鎖させる場面です。公式の例を見てください。

bob?.department?.head?.name

公式によれば、この連鎖はどれか1つでもnullなら全体がnullを返します。段階ごとにnullチェックを書く必要がありません。

道具3:エルビス演算子

「nullだったら代わりにこの値を使う」を短く書く演算子です。

val b: String? = null
val l: Int = b?.length ?: 0

公式の定義は明快です。?:の左側がnullでなければそれを返し、nullなら右側の式を返します。そして右側は左側がnullのときだけ評価されます。

さらに実用的な使い方があります。公式には「Kotlinではthrowとreturnが式であるため、エルビス演算子の右側に置ける」と記されています。

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}

引数の妥当性を確認する場面で、この書き方は非常に読みやすくなります。

道具4:非null表明演算子

ここが最も注意すべき部分です。公式によれば、!!は「任意の値をnull不可の型に変換する」演算子です。

val b: String? = "Kotlin"
val l = b!!.length   // 6

値がnullでなければ問題なく動きます。しかしnullだった場合は次のようになります。

val b: String? = null
val l = b!!.length
// Exception in thread "main" java.lang.NullPointerException

公式の説明どおり、!!はnullであっても強制的にnull不可として扱わせるため、例外が発生します。

では、いつ使うのか。公式は用途を限定して書いています。

!!演算子は、値がnullでないと確信があり例外が起きる余地がないにもかかわらず、特定の規則のためにコンパイラがそれを保証できない場合に特に有用である

「エラーを消すために付ける」ものではありません。コンパイラのエラーを!!で黙らせる書き方は、null安全の仕組みを自ら無効にする行為です。学習段階では、!!を書きたくなったら一度立ち止まり、?.か?:で書けないかを考えてください。

lateinitと初期化の例外

公式には、nullに関連するもう1つの例外としてUninitializedPropertyAccessExceptionが挙げられています。まだ初期化されていないプロパティにアクセスしようとしたときにKotlinが投げる例外で、公式によれば典型的にはlateinitプロパティで発生します。

公式の説明では、この例外はnull不可のプロパティが準備できるまで使われないことを保証する役割を持ちます。「あとで必ず入れるから」とlateinitを多用すると、null安全が守ってくれる範囲から外れていきます。

使い分けの指針

状況 使う道具
nullなら処理をしない ?.
nullなら代わりの値を使う ?:
nullなら関数を抜ける・例外を投げる ?:とreturn/throw
条件が複雑 ifによるチェック
絶対にnullでないと確信がある !!(最後の手段)

まとめ

  • null安全は、null関連の問題をコンパイル時に捕まえる仕組み
  • 型の後ろの?が「nullを許す」印
  • ?.はnullならnullを返し、連鎖できる
  • ?:はnullのときの代替値を指定する
  • !!は例外の原因になりうるため、確信がある場合に限る

基本構文はKotlinの使い方|基本文法・基本構文をサンプルコードで解説、例外処理はKotlinのエラー処理で解説しています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら