Skip to content

ヌル安全性

ヌル安全性は、ヌル参照(「10億ドルの間違い (The Billion-Dollar Mistake)」としても知られています)の発生リスクを大幅に削減するために設計されたKotlinの機能です。

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

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

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

KotlinでNPEが発生しうる唯一の原因は次のとおりです。

  • throw NullPointerException()の明示的な呼び出し。
  • 非ヌル表明演算子!!の使用。
  • 初期化中のデータ不整合。例えば、以下の場合です。
  • Javaとの相互運用:
    • プラットフォーム型のヌル参照のメンバーにアクセスしようとすること。
    • ジェネリック型におけるヌル許容性の問題。例えば、KotlinのMutableList<String>nullを追加するJavaコードがあり、これを適切に処理するにはMutableList<String?>が必要になる場合など。
    • 外部のJavaコードによって引き起こされるその他の問題。

TIP

NPE以外にも、ヌル安全性に関連する別の例外として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は非ヌル許容変数であるため、NPEを引き起こすことなく、aのメソッドを安全に呼び出すか、プロパティにアクセスできます。コンパイラは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
}

bに直接lengthにアクセスしようとすると、コンパイラはエラーを報告します。これは、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
}

上記の例では、コンパイラがスマートキャストを実行して、型をヌル許容型の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が変更されないことを保証できる場合にのみ機能することに注意してください。

安全呼び出し演算子

安全呼び出し演算子?.を使用すると、より短い形式でヌル許容性を安全に処理できます。オブジェクトがnullの場合、NPEをスローする代わりに、?.演算子は単純に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の場合にのみ評価されます。

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

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ではないことをコンパイラに明示的に伝えることができます。

ヌル許容レシーバー

ヌル許容レシーバー型を持つ拡張関数を使用すると、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 
    }
}

安全キャスト

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]
}

次は何ですか?