Skip to content

ヌル安全

ヌル安全 (Null safety) は、10億ドルの過ち (The Billion-Dollar Mistake) とも呼ばれるヌル参照のリスクを大幅に軽減するために設計されたKotlinの機能です。

Javaを含む多くのプログラミング言語における最も一般的な落とし穴の1つは、ヌル参照のメンバーにアクセスするとヌル参照例外が発生することです。Javaでは、これは NullPointerException、または略して NPE に相当します。

Kotlinは、型システムの一部としてヌル許容性 (nullability) を明示的にサポートしており、どの変数やプロパティがnullを許容するかを明示的に宣言できます。また、非ヌル許容変数を宣言すると、コンパイラはこれらの変数がnull値を保持できないように強制し、NPEを防止します。

Kotlinのヌル安全は、潜在的なヌル関連の問題を実行時ではなくコンパイル時に捕捉することで、より安全なコードを保証します。この機能は、null値を明示的に表現することでコードの堅牢性、可読性、保守性を向上させ、コードを理解し管理しやすくします。

KotlinでNPEが発生する唯一の可能性のある原因は次のとおりです。

NPEの他に、ヌル安全に関連するもう1つの例外は UninitializedPropertyAccessException です。Kotlinは、初期化されていないプロパティにアクセスしようとしたときにこの例外をスローし、非ヌル許容プロパティが準備できるまで使用されないことを保証します。これは通常、lateinitプロパティで発生します。

ヌル許容型と非ヌル許容型

Kotlinでは、型システムはnullを保持できる型 (ヌル許容型) と保持できない型 (非ヌル許容型) を区別します。例えば、通常のString型の変数はnullを保持できません。

kotlin
fun main() {
    // 非ヌル文字列を変数に代入します
    var a: String = "abc"
    // 非ヌル許容変数にnullを再代入しようとします
    a = null
    print(a)
    // Null can not be a value of a non-null type String
}

aに対してメソッドを安全に呼び出したり、プロパティにアクセスしたりできます。aは非ヌル許容変数であるため、NPEを引き起こさないことが保証されています。コンパイラはaが常に有効なString値を保持することを保証するため、nullのときにそのプロパティやメソッドにアクセスするリスクはありません。

kotlin
fun main() {
    // 非ヌル文字列を変数に代入します
    val a: String = "abc"
    // 非ヌル許容変数の長さを返します
    val l = a.length
    print(l)
    // 3
}

null値を許可するには、変数型の直後に?記号を付けて変数を宣言します。例えば、String?と記述することでヌル許容文字列を宣言できます。この表現によりStringnullを受け入れられる型になります。

kotlin
fun main() {
    // ヌル許容文字列を変数に代入します
    var b: String? = "abc"
    // ヌル許容変数にnullを正常に再代入します
    b = null
    print(b)
    // null
}

blengthに直接アクセスしようとすると、コンパイラはエラーを報告します。これは、bがヌル許容変数として宣言されており、null値を保持できるためです。ヌル許容型でプロパティに直接アクセスしようとすると、NPEが発生します。

kotlin
fun main() {
    // ヌル許容文字列を変数に代入します
    var b: String? = "abc"
    // ヌル許容変数にnullを再代入します
    b = null
    // ヌル許容変数の長さを直接返そうとします
    val l = b.length
    print(l)
    // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? 
}

上記の例では、コンパイラはプロパティにアクセスしたり操作を実行したりする前に、ヌル許容性をチェックするために安全な呼び出しを使用することを要求します。ヌル許容型を処理するにはいくつかの方法があります。

null処理のツールとテクニックの詳細と例については、次のセクションをお読みください。

if条件式によるnullチェック

ヌル許容型を扱う場合、NPEを避けるためにヌル許容性を安全に処理する必要があります。これを処理する1つの方法は、if条件式を使ってヌル許容性を明示的にチェックすることです。

例えば、bnullかどうかをチェックし、それからb.lengthにアクセスします。

kotlin
fun main() {
    // ヌル許容変数にnullを代入します
    val b: String? = null
    // 最初にヌル許容性をチェックし、それから長さにアクセスします
    val l = if (b != null) b.length else -1
    print(l)
    // -1
}

上記の例では、コンパイラはスマートキャスト (smart cast)を実行し、ヌル許容のString?型を非ヌル許容のString型に変更します。また、実行したチェックに関する情報を追跡し、if条件式内でlengthへの呼び出しを許可します。

より複雑な条件もサポートされています。

kotlin
fun main() {
    // ヌル許容文字列を変数に代入します
    val b: String? = "Kotlin"

    // 最初にヌル許容性をチェックし、それから長さにアクセスします
    if (b != null && b.length > 0) {
        print("String of length ${b.length}")
        // String of length 6
    } else {
        // 条件が満たされない場合の代替を提供します
        print("Empty string")
    }
}

上記の例は、スマートキャストの前提条件と同じく、コンパイラがチェックから使用までの間にbが変更されないことを保証できる場合にのみ機能することに注意してください。

安全呼び出し演算子

安全呼び出し演算子 ?. を使用すると、より短い形式でヌル許容性を安全に処理できます。NPEをスローする代わりに、オブジェクトがnullの場合、?.演算子は単にnullを返します。

kotlin
fun main() {
    // ヌル許容文字列を変数に代入します
    val a: String? = "Kotlin"
    // ヌル許容変数にnullを代入します
    val b: String? = null
    
    // ヌル許容性をチェックし、長さまたはnullを返します
    println(a?.length)
    // 6
    println(b?.length)
    // null
}

b?.lengthという式はヌル許容性をチェックし、bが非ヌルであればb.lengthを返し、そうでなければnullを返します。この式の型はInt?です。

Kotlinでは、?.演算子をvar変数とval変数の両方で使用できます。

  • ヌル許容のvarnull (例: var nullableValue: String? = null) または非ヌル値 (例: var nullableValue: String? = "Kotlin") を保持できます。非ヌル値である場合、いつでもnullに変更できます。
  • ヌル許容のvalnull (例: val nullableValue: String? = null) または非ヌル値 (例: val nullableValue: String? = "Kotlin") を保持できます。非ヌル値である場合、後でnullに変更することはできません。

安全な呼び出しはチェーンで役立ちます。例えば、Bobは部署に配属されているかもしれない (あるいはいないかもしれない) 従業員です。その部署には、さらに別の従業員が部署の責任者として配属されているかもしれません。Bobの部署の責任者の名前を取得するには (もしいるならば)、次のように記述します。

kotlin
bob?.department?.head?.name

このチェーンは、いずれかのプロパティがnullであればnullを返します。

代入の左側に安全な呼び出しを配置することもできます。

kotlin
person?.department?.head = managersPool.getManager()

上記の例では、安全呼び出しチェーン内のレシーバーのいずれかがnullの場合、代入はスキップされ、右側の式はまったく評価されません。例えば、personまたはperson.departmentのいずれかがnullの場合、関数は呼び出されません。以下は、同じ安全呼び出しをif条件式で記述した場合の同等なものです。

kotlin
if (person != null && person.department != null) {
    person.department.head = managersPool.getManager()
}

エルビス演算子

ヌル許容型を扱う場合、nullをチェックし、代替値を提供できます。例えば、bnullでない場合、b.lengthにアクセスします。そうでなければ、代替値を返します。

kotlin
fun main() {
    // ヌル許容変数にnullを代入します  
    val b: String? = null
    // ヌル許容性をチェックします。nullでなければ長さを返し、nullであれば0を返します
    val l: Int = if (b != null) b.length else 0
    println(l)
    // 0
}

完全なif式を記述する代わりに、エルビス演算子?:を使って、より簡潔な方法でこれを処理できます。

kotlin
fun main() {
    // ヌル許容変数にnullを代入します  
    val b: String? = null
    // ヌル許容性をチェックします。nullでなければ長さを返し、nullであれば非ヌル値を返します
    val l = b?.length ?: 0
    println(l)
    // 0
}

?:の左側の式がnullでない場合、エルビス演算子はそれを返します。そうでなければ、エルビス演算子は右側の式を返します。右側の式は、左側がnullの場合にのみ評価されます。

Kotlinではthrowreturnは式であるため、エルビス演算子の右側でも使用できます。これは、例えば関数の引数をチェックする際に便利です。

kotlin
fun foo(node: Node): String? {
    // getParent()をチェックします。nullでなければparentに代入され、nullであればnullを返します
    val parent = node.getParent() ?: return null
    // getName()をチェックします。nullでなければnameに代入され、nullであれば例外をスローします
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}

非ヌルアサーション演算子

非ヌルアサーション演算子 !! は、あらゆる値を非ヌル許容型に変換します。

!!演算子をnullではない値を持つ変数に適用すると、それは非ヌル許容型として安全に処理され、コードは正常に実行されます。しかし、値がnullの場合、!!演算子はそれを非ヌル許容として強制的に扱わせ、その結果NPEが発生します。

bnullではなく、!!演算子がその非ヌル値 (この例ではString) を返させると、lengthに正しくアクセスします。

kotlin
fun main() {
    // ヌル許容文字列を変数に代入します
    val b: String? = "Kotlin"
    // bを非ヌルとして扱い、その長さにアクセスします
    val l = b!!.length
    println(l)
    // 6
}

bnullの場合、!!演算子がその非ヌル値を返させると、NPEが発生します。

kotlin
fun main() {
    // ヌル許容変数にnullを代入します  
    val b: String? = null
    // bを非ヌルとして扱い、その長さにアクセスしようとします
    val l = b!!.length
    println(l) 
    // Exception in thread "main" java.lang.NullPointerException
}

!!演算子は、値がnullではないと確信しており、NPEが発生する可能性がない場合、しかしコンパイラが特定のルールによりこれを保証できない場合に特に便利です。そのような場合、!!演算子を使用して、値がnullではないことをコンパイラに明示的に伝えることができます。

ヌル許容レシーバー

ヌル許容レシーバー型 (nullable receiver type)を持つ拡張関数を使用でき、これにより、nullになる可能性のある変数でこれらの関数を呼び出すことができます。

ヌル許容レシーバー型に拡張関数を定義することで、関数を呼び出すすべての場所でnullをチェックする代わりに、関数内でnull値を処理できます。

例えば、.toString()拡張関数はヌル許容レシーバーで呼び出すことができます。null値で呼び出された場合、例外をスローすることなく、安全に文字列"null"を返します。

kotlin
fun main() {
    // person変数に格納されたヌル許容Personオブジェクトにnullを代入します
    val person: Person? = null

    // ヌル許容person変数に.toStringを適用し、文字列を出力します
    println(person.toString())
    // null
}

// シンプルなPersonクラスを定義します
data class Person(val name: String)

上記の例では、personnullであっても、.toString()関数は安全に文字列"null"を返します。これはデバッグやログ記録に役立ちます。

.toString()関数がヌル許容文字列 (文字列表現またはnullのいずれか) を返すことを期待する場合、安全呼び出し演算子 ?.を使用します。?.演算子は、オブジェクトがnullでない場合にのみ.toString()を呼び出し、そうでなければnullを返します。

kotlin
fun main() {
    // ヌル許容Personオブジェクトを変数に代入します
    val person1: Person? = null
    val person2: Person? = Person("Alice")

    // personがnullの場合に"null"を出力します。そうでない場合はperson.toString()の結果を出力します
    println(person1?.toString())
    // null
    println(person2?.toString())
    // Person(name=Alice)
}

// Personクラスを定義します
data class Person(val name: String)

?.演算子を使用すると、nullになる可能性のあるオブジェクトのプロパティや関数にアクセスしながら、潜在的なnull値を安全に処理できます。

let関数

null値を処理し、非ヌル型に対してのみ操作を実行するには、安全呼び出し演算子 ?.let関数と組み合わせて使用できます。

この組み合わせは、式を評価し、結果がnullかどうかをチェックし、nullでない場合にのみコードを実行するのに役立ち、手動でのヌルチェックを回避します。

kotlin
fun main() {
    // ヌル許容文字列のリストを宣言します
    val listWithNulls: List<String?> = listOf("Kotlin", null)

    // リスト内の各項目を反復処理します
    for (item in listWithNulls) {
        // 項目がnullかどうかをチェックし、非ヌル値のみを出力します
        item?.let { println(it) }
        //Kotlin 
    }
}

安全なキャスト

型キャスト (type casts)のための通常のKotlin演算子はas演算子です。しかし、オブジェクトがターゲット型でない場合、通常のキャストは例外を引き起こす可能性があります。

安全なキャストにはas?演算子を使用できます。これは値を指定された型にキャストしようとします。そして、値がその型でない場合はnullを返します。

kotlin
fun main() {
    // あらゆる型の値を保持できるAny型の変数を宣言します
    val a: Any = "Hello, Kotlin!"

    // as?演算子を使用してIntへの安全なキャストを行います
    val aInt: Int? = a as? Int
    // as?演算子を使用してStringへの安全なキャストを行います
    val aString: String? = a as? String

    println(aInt)
    // null
    println(aString)
    // "Hello, Kotlin!"
}

上記のコードはnullを出力します。aIntではないため、キャストは安全に失敗します。また、String?型と一致するため、安全なキャストは成功し、"Hello, Kotlin!"を出力します。

ヌル許容型のコレクション

ヌル許容要素のコレクションがあり、非ヌルなものだけを保持したい場合、filterNotNull()関数を使用します。

kotlin
fun main() {
    // null値と非nullの整数値を含むリストを宣言します
    val nullableList: List<Int?> = listOf(1, 2, null, 4)

    // null値をフィルタリングし、非nullの整数リストを生成します
    val intList: List<Int> = nullableList.filterNotNull()
  
    println(intList)
    // [1, 2, 4]
}

次のステップ