プログラム

KotlinのNull安全ってどう安全なの?! 検証も交えてまとめてみた!

Table Of Contents

  1. KotlinのNull安全とはどういうものなのか
  2. どのようにNull安全が保証されているのか
  3. Null可能型の安全な使い方① - 条件式でnullを確認 -
  4. Javaで作成したクラスでの検証
  5. Kotlinで作成したクラスにあるミュータブルでNull可能なプロパティにJavaでnullを代入して返すメソッドを作り、Kotlinでnullの入ったプロパティを使う
  6. Kotlinで作成したクラスにあるミュータブルで非NullなプロパティにJavaでnullを代入して返すメソッドを作り、Kotlinでnullの入ったプロパティを使う
  7. Null可能型の安全な使い方② - ?.演算子で安全な呼び出し -
  8. Null可能型の安全な使い方③ - エルビス演算子で安全な呼び出し -
  9. Null安全なんてどうでもいい! 俺はNullPointerExceptionを発生させたいんだ!
  10. Null可能型を使用した安全なキャスト
  11. Null可能型のコレクションを安全に使用する
  12. その他のNull安全な機能
  13. KotlinのNull安全はすごい、だけど…
  14. KotlinでNullPointerExceptionが発生する場面
  15. 初期化に関してのデータの不一致がある(コンストラクター内のどこかで、利用可能な初期化されていないオブジェクトが使用される)
  16. まとめ

Androidの公式言語になったKotlinですが、Null安全(Null Safety)に設計された言語になっています。
どのようにNull安全が保証されているのかについてまとめました。

また、記事の途中にKotlinのNull安全がどれほどのものなのかの検証、実験を行なっています。
先にその検証を読みたい方はここからどうぞ。

それではKotlinのNull安全について見ていきましょう!

参考にしたページは以下になります。
Null Safety

KotlinのNull安全とはどういうものなのか

KotlinのNull安全は、プログラムの実行時にNullPointerExceptionが発生する可能性を極力排除されるように設計されています。

KitlinではNull参照に関する構文が厳しく設定されており、それらが守られていない場合、コンパイルエラーになるようになっています。
このNull参照に関する構文の厳しさにより、NullPointerExceptionが発生する可能性を排除しています。

そのため、KotlinはNull安全になります。

どのようにNull安全が保証されているのか

KotlinのNull安全はいくつかの方法で保証されています。
その方法を実際のコードを交えながら説明していきたいと思います。

非Null型とNull可能型の使い分けでNull安全が保証される

Kotlinの型体系には、非Null型とNull可能型があります。

非Null型はnull参照を保持することができません。
Null可能型はnull参照を保持することができます。
null参照を保持できない型とnull参照を保持できる型を区別することにより、KotlinはNull安全を保証しています。

では、非Null型はどういった型なのでしょうか?
それは、よく僕たちが見かける型が非null型になります。
String、Int、Boolean、Person(自分で作ったクラス)などが非Null型になります。

それではNull可能型とはどういった型なのでしょうか?
各型に?をつけることでNull可能型となります。
String?、Int?、Boolean?、Person?などのようにすることでnull参照可能な型となります。

それでは実際にどのようにnull安全が保障されるのかを見ていきましょう。
例えば、通常のString型(非Null型)はnullを保持できないため、以下のようにnullを保持しようとするとコンパイルエラーになります。

var nonNullString: String = "abc"
nonNullString = null //ここでコンパイルエラー

nullを許可するためにはString?と記述し、Null可能型として宣言する必要があります。
以下のようにString?で宣言した場合、nullを保持することができます。

var nullableString: String? = "abc"
nullableString = null //コンパイルエラーにならない

コードを見て気づかれている方も多いかと思いますが、普通に変数を宣言する場合、?をつけないので非Null型の宣言となります。
なので特に意識せずに宣言した変数は必ずnullが保持できないことになるので、普通に宣言した変数のプロパティやメソッドなどにアクセスする際にNullPointerExceptionが発生しないことを保証します。
これは思いもよらないnullの参照を防ぎ、思いもよらないバグを防ぎます。

// 非Null型なのでnull参照することなく、安全にlengthプロパティを呼び出せる
val length: Int = nonNullString.length

ここまで読んだ方はこう思われるかもしれません。
Null可能型でnullを保持させて、その変数のプロパティやメソッドを呼び出したら結局NullPointerExceptionが発生するじゃん、と。
Kotlinではそこも上手くNull安全が保証されています。

例えば、Null可能型で普通にプロパティやメソッドを呼び出そうとするとコンパイルエラーになるようになっています。

val length: Int = nullableString.length

nullableStringはNull可能型で、nullの可能性があります。
安全ではないため、コンパイラはエラーを発生させます。

では、どのようにすればコンパイルエラーを発生させずに使用することができるのでしょうか?
いくつかあるNull可能型の使い方を見ていきましょう。

Null可能型の安全な使い方① - 条件式でnullを確認 -

Null可能型は、条件式でnullの確認を行うことでコンパイルエラーを発生させずに使用することができます。

val length: Int = if (nullableString != null) nullableString.length else -1

nullではない場合、nullableString.lengthの値が代入されます。
そしてこの例のように、変数nullableStringがnullだった場合、任意の値を代入することもできます。

コンパイラはNull可能型の確認についての情報を解析し、ifの中でのlengthへの呼び出しを許可するそうです。

より複雑な条件式にも対応しています。

if (nullableString != null && nullableString.length > 0) {
    println("String of length ${nullableString.length}")
} else {
    println("Empty string")
}

上記の例ではifの条件で先にnullの確認を行なっているため、次の条件でlengthの呼び出しが許可されます。

プロパティやメソッドの呼び出しだけでなく、Null可能なInt型の場合もnullの確認が必要になります。

// 条件式でnullの確認を行わないとコンパイルエラーになる
var nullableIntA: Int? = 1
val summedNullableIntA = nullableIntA + 1

// 条件式でnullの確認を行うと変数を使用することができる
var nullableIntB: Int? = 1
val summedNullableIntB = if (nullableIntB != null) nullableIntB + 1 else -1

この使用方法は以下の場合にのみ動作することに注意してください、とKotlinの公式リファレンスに記載がありました。

  • nullの確認をしてから使用するまでの間に更新されていないローカル変数
  • バッキングフィールドを持ち、上書き不可能なvalプロパティ

なぜならば、他の場合、null確認をした後、nullに変更されることが発生する可能性があるからです、とも記載がありました。

書いている意味はわかったのですが、本当にそうなのか、実際に検証してみました。

ローカル変数でnull確認後、値を代入し使用してみる

null確認後、ローカル変数にnullを代入してみました。

var nullableString: String? = "nullableString"
if (nullableString != null) {
    nullableString = null
    println("String of length ${nullableString.length}")
}

結果はコンパイルエラーになります。
一つ目の条件の「nullの確認をしてから使用するまでの間に更新されていないローカル変数」に当てはまっていないからでしょうか?

次にnull確認後、ローカル変数にnullでない値を代入してみました。

var nullableString: String? = "nullableString"
if (nullableString != null) {
    nullableString = "update"
    println("String of length ${nullableString.length}")
}

これも「nullの確認をしてから使用するまでの間に更新されていないローカル変数」に当てはまっていないように思えます。
ですが、結果はコンパイル成功でした。
そしてプログラムを実行したところ、正常に動作しました。

それでは、null確認後、ローカル変数にnullでない値を代入し、更にnullでない値を代入してみました。

var nullableString: String? = "nullableString"
if (nullableString != null) {
    nullableString = "update"
    nullableString = "update2"
    println("String of length ${nullableString.length}")
}

同じく、コンパイル成功でプログラムも動作します。
ですが、コンパイル時に、変数nullableStringに割り当てられた"update"は使われることがないですよ、という警告が表示されました。
地味に優秀なコンパイラ(笑

最後に、null確認後、ローカル変数にnullでない値を代入し、nullを代入してみました。

var nullableString: String? = "nullableString"
if (nullableString != null) {
    nullableString = "update"
    nullableString = null
    println("String of length ${nullableString.length}")
}

まぁ半分予想がついていましたが、結果はコンパイルエラーでした。

Null可能型のローカル変数を条件式の確認後に使用する場合、確認後にnullが代入されていなければコンパイルエラーにならず、プログラムは正常に動作することがわかりました。

プロパティでnull確認後、値を代入し使用してみる

それではクラスに定義されたプロパティを使用して検証してみたいと思います。

まずはvalで定義されたプロパティにnullを代入してみました。

/**
 * Null可能型のプロパティを持ったクラス
 */
class NullablePropertyHolder {
    val nullableProperty: String? = "nullableProperty"
}

/**
 * Null可能型のプロパティを実際に使用する
 */
fun main(args: Array<string>) {
    val nullablePropertyHolder = NullablePropertyHolder()
    if (nullablePropertyHolder.nullableProperty != null) {
        nullablePropertyHolder.nullableProperty = null
        val length = nullablePropertyHolder.nullableProperty.length
        println("String of length ${length}")   
    }
}

上記の結果はコンパイルエラーです。
nullablePropertyにnullを代入しようとした時点でコンパイルエラーになります。

これはNull安全の機能とは異なり、val(上書き不可能なプロパティ)で宣言されたプロパティに新たに値を代入しようとしたことによってのコンパイルエラーになります。

話がそれますが、Kotlinはイミュータビリティに特化した言語で上記のようにvalで宣言したオブジェクトに対しての再代入を許可していません。
詳しくは以下の記事を参照してください。

プログラム

Android公式言語 Kotlinとはどんな言語でどんなメリットがあるのか?! サムネイル

Android公式言語 Kotlinとはどんな言語でどんなメリットがあるのか?!

2017/10/14

実は上記のソースコードをコンパイルするともう一つコンパイルエラーが発生します。
新たな値(null)の代入はvalなので許可されてませんが、nullが代入されたとしてコンパイラが判断し、nullが代入されたオブジェクトを参照しようとしてますよ、と親切にも文字列の長さを参照している箇所でコンパイルエラーを発生させます。
なかなかに厳し目なコンパイラですね。

またまた話がそれますが、Kotlinのクラスのプロパティは、ただプロパティの宣言をするだけで自動的にバッキングフィールドが作成されるようになっています。
セッターとゲッターも定義しなくても自動で作成してくれます。

逆に、Kotlinでは自分で作成したフィールドを持つことはできませんので注意してください。
その代わり、自分で定義したセッター、ゲッター内でfield識別子を使用して自動で作成されたバッキングフィールドにアクセスできますし、プロパティをバッキングプロパティとしてセッター、ゲッター内で使用することができます。
詳しくはここに記載があります。

ちなみにバッキングフィールドとは、プロパティを通してアクセスできるprivteなフィールドのことを指します。

ちなみのちなみに、Kotlinのクラスのインスタンスの生成にはnew修飾子の記述は必要ないです。
いちいちnewを書かなくていいのでスマート!

それでは次は、上書き可能なプロパティで検証してみましょう。

上書き可能なプロパティにnullを代入してみました。

/**
 * Null可能型でミュータブルなプロパティを持ったクラス
 */
class NullablePropertyHolder {
    var nullableProperty: String? = "nullableProperty"
}

// Null可能型のプロパティを実際に使用する
fun main(args: Array<string>) {
    val nullablePropertyHolder = NullablePropertyHolder()
    if (nullablePropertyHolder.nullableProperty != null) {
        nullablePropertyHolder.nullableProperty = null
        val length = nullablePropertyHolder.nullableProperty.length
        println("String of length ${length}")   
    }
}

結果はコンパイルエラーでした。
これは二つ目の条件「バッキングフィールドを持ち、上書き不可能なvalプロパティ」に当てはまらないためでしょうか。
コンパイルエラーの内容はnullを参照しようとしていますよ、というエラーでした。

次は上書き可能なプロパティにnullでない値を代入してみました。

/**
 * Null可能型のプロパティを実際に使用する
 */
fun main(args: Array<string>) {
    val nullablePropertyHolder = NullablePropertyHolder()
    if (nullablePropertyHolder.nullableProperty != null) {
        nullablePropertyHolder.nullableProperty = "update"
        val length = nullablePropertyHolder.nullableProperty.length
        println("String of length ${length}")   
    }
}

結果はコンパイルエラーでした。
ローカル変数と同じ結果になるかと思っていましたが、予想と反してコンパイルエラーになりました。

コンパイルエラーの内容は、ミュータブル(変更可能)なオブジェクトをこの方法では参照できません、というエラーでした。
二つ目の条件「バッキングフィールドを持ち、上書き不可能なvalプロパティ」に当てはまらないため、コンパイルエラーになったということがわかりますね。

この結果から、クラスにあるミュータブルなオブジェクトを参照する場合、条件式を使った方法では、Null可能型のオブジェクトは参照できないことがわかりました。

Kotlinの公式のリファレンスのとおり、条件式を使ったNull可能型のオブジェクトへの参照には、Null安全が保証されているようです。

他にも、検証してみました。

Javaで作成したクラスでの検証

KotlinはJavaとの相互運用ができますのでJavaのクラスを使った検証を行いたいと思います。

先ほどKotlinで使用していたクラスに相当するクラスをJavaに定義し、そのクラスのプロパティを条件式を使わずに使用するとどうなるのか試してみます。

// javasorce/test/NullablePropertyHolder.java

package javasorce.test;

/**
 * KotlinでNull可能型でミュータブルなプロパティを持ったクラスに変換されるクラス
 */
public class NullablePropertyHolder {
    /**
     * Null可能型のプロパティ
     */
    private String nullableProperty = "nullableProperty";

    /**
     * Null可能型のプロパティ getter
     */
    public String getNullableProperty() {
        return nullableProperty;
    }

    /**
     * Null可能型のプロパティ setter
     */
    public void setNullableProperty(String value) {
        nullableProperty = value;
    }
}
// kotlinsource/CallNullablePropertyHolderFromJava.kt

package kotlinsorce

import javasorce.test.NullablePropertyHolder

/**
 * NullablePropertyHolderをJavaから呼び出しプロパティを使用する
 */
fun main(args: Array<string>) {
    val nullablePropertyHolder = NullablePropertyHolder()
    val length = nullablePropertyHolder.nullableProperty.length
    println("length = ${length}")
}

結果はコンパイル成功でした。
そして実行したところ、エラーが発生することなくプログラムが動作しました。

今回のように、Javaから呼び出すクラスにString型のようなnull参照できる型がプロパティに含まれている場合、KotlinのNull安全は機能しません。
なので、Javaと同じようにプロパティを条件式でnull確認することなく、String型のような参照型を使用することができます。
その代わり、NullPointerExceptionもJavaで動作させるのと同じように発生します。
要はJavaと同じ動作になるということですね。

なので、以下のようにすると実行時にNullPointerExceptionが発生します。

val nullablePropertyHolder = NullablePropertyHolder()
nullablePropertyHolder.nullableProperty = null

// ここでNullPointerExceptionが発生する
val length = nullablePropertyHolder.nullableProperty.length

Kotlinでは、Javaで定義されたクラス(String型なども含む)はプラットフォーム型と呼んでいるようです。
全てのプラットフォーム型は、KotlinのNull安全の機能が働かなく、Javaと同じようにNullPointerExceptionが発生します。
例えば、NullablePropertyHolderにStringの代わりにJavaで定義されたクラスをプロパティとして持っていた場合、そのプロパティはnullチェックすることなく使用することができ、null参照するとNullPointerExceptionが発生します。
詳しくはここに記載があります。

ですが、プラットフォーム型であっても、Kotlinのコード内で非Null型として宣言して使用する場合、その参照自体にはnullを代入することはできません。

// 非Null型として宣言しているのでnullを代入するとコンパイルエラーになる
val nullablePropertyHolder: NullablePropertyHolder = null

話はそれますが、ここに記載しているプログラムは全てコマンドラインでコンパイルし実行しています。
KotlinからJavaのクラスをimportしてコマンドラインで動作させようとしたところで上手くいかず数時間はまってしまいました(笑
生まれて初めてStack Overflowで質問してしまいました。
だって、Javaのjarクラスを含んだKotlinのプログラムをコマンドラインで動かす方法が全然見つからなかったんだもん(笑

かいつまんでいうと、Kotlinが参照しているJavaのクラスを呼び出そうとするとクラスが見つからないエラーが出てしまうという現象に陥りました。
Stack Overflowにて親切なナイスガイの回答を読んでわかったのですが、Javaでいうところの、実行するjarが依存しているjarは全てクラスパスに渡す必要がある、ということでした。
Kotlinでも同じで、依存しているjarを読み込まないとクラスが見つからないということですね。

Javaのjarをコマンドラインで動かしたことなかったので勉強になりました。
後日、このことをまとめて記事にしたい思っています。

ちなみに、Stack Overflowでの僕の質問はここになります。

Kotlinで作成したクラスにあるミュータブルでNull可能なプロパティにJavaでnullを代入して返すメソッドを作り、Kotlinでnullの入ったプロパティを使う

タイトルのとおり、検証したいと思います。

// NullablePropertyHolder.kt

package kotlinsorce

/**
 * Null可能型でミュータブルなプロパティを持ったクラス
 */
class NullablePropertyHolder {
    var nullableProperty: String? = "nullableProperty"
}
// GetSetNullablePropertyHolder.java

package javasorce.test;

import kotlinsorce.NullablePropertyHolder;

/**
 * Kotlinで定義したクラスのプロパティにnullを代入して返すクラス
 */
public class GetSetNullablePropertyHolder {
    /**
     * Kotlinで定義したクラスのプロパティにnullを代入して返す
     */
    public NullablePropertyHolder getNullablePropertyHolderWithNullInProperty() {
        NullablePropertyHolder nullablePropertyHolder = new NullablePropertyHolder();
        nullablePropertyHolder.setNullableProperty(null);
        return nullablePropertyHolder;
    }
}
// CallGetSetNullablePropertyHolder.kt

package kotlinsorce

import javasorce.test.GetSetNullablePropertyHolder

/**
 * KotlinのNullablePropertyHolderクラスにJavaでnullが代入されたプロパティを使用する
 */
fun main(args: Array<string>) {
    val getSetNullablePropertyHolder = GetSetNullablePropertyHolder()
    val nullablePropertyHolder = getSetNullablePropertyHolder.getNullablePropertyHolderWithNullInProperty()
    val length = nullablePropertyHolder.nullableProperty.length
    println("length = ${length}")
}

結果はコンパイルエラーでした。
Kotlinで定義したクラスを使用しているため、いくらJavaでnullを代入しようが、Null安全が働き、コンパイル時に、Null参照している可能性があるオブジェクトを使おうとしている、といったエラーが発生します。

条件式でnull確認を行ってみましたが、こちらもコンパイルエラーになりました。

val length = if (nullablePropertyHolder.nullableProperty != null) nullablePropertyHolder.nullableProperty.length else -1

これは二つ目の条件「バッキングフィールドを持ち、上書き不可能なvalプロパティ」に当てはまらないため、条件式でのnull確認をしてもコンパイルエラーになるということでした。

また、Javaで定義したgetNullablePropertyHolderWithNullInProperty()をKotlinで使用し、型指定をしなかった場合、デフォルトではKotlinで定義したNullablePropertyHolderクラスの非Null型として推測され、参照を受け取ります。

Java側のgetNullablePropertyHolderWithNullInProperty()でNullablePropertyHolderクラスをnullで返した場合、NullablePropertyHolderのインスタンスを後述する?.演算子を使って参照した場合、IllegalStateExceptionが発生します。

val length = nullablePropertyHolder.nullableProperty?.length ?: -1

Kotlinで作成したクラスにあるミュータブルで非NullなプロパティにJavaでnullを代入して返すメソッドを作り、Kotlinでnullの入ったプロパティを使う

次の検証は、非Nullなオブジェクトとして定義されたプロパティにJava側でnullを代入し、そのプロパティを使用してみます。
どんな結果になるのでしょうね。
ワクワクしますね。

// NonNullPropertyHolder.kt

package kotlinsorce

/**
 * 非Null型でミュータブルなプロパティを持ったクラス
 */
class NonNullPropertyHolder {
    var nonNullProperty: String = "nonNullProperty"
}
// GetSetNonNullPropertyHolder.java

package javasorce.test;

import kotlinsorce.NonNullPropertyHolder;

/**
 * Kotlinで定義したクラスのプロパティにnullを代入して返すクラス
 */
public class GetSetNonNullPropertyHolder {
    /**
     * Kotlinで定義したクラスのプロパティにnullを代入して返す
     */
    public NonNullPropertyHolder getNonNullPropertyHolderWithNullInProperty() {
        NonNullPropertyHolder nonNullPropertyHolder = new NonNullPropertyHolder();
        nonNullPropertyHolder.setNonNullProperty(null);
        return nonNullPropertyHolder;
    }
}
// CallGetSetNonNullPropertyHolder.kt

package kotlinsorce

import javasorce.test.GetSetNonNullPropertyHolder

/**
 * KotlinのNonNullPropertyHolderクラスにJavaでnullが代入されたプロパティを使用する
 */
fun main(args: Array<string>) {
    val getSetNonNullPropertyHolder = GetSetNonNullPropertyHolder()
    val nonNullPropertyHolder = getSetNonNullPropertyHolder.getNonNullPropertyHolderWithNullInProperty()
    val length = nonNullPropertyHolder.nonNullProperty.length
    println("length = ${length}")
}

まずコンパイルはエラーになることなく、成功しました。
Kotlinで定義しているNonNullPropertyHolderクラスのnonNullPropertyは非Null型のため、そもそもコンパイラはKotlinのソースコードでnull参照が行われていないと判断します。
今回のようにJava側で非Null型のオブジェクトにnullを代入されていようが、Kotlinのコンパイラはそれを感知しません。

ですが実行したところ、以下のようなエラーが発生しました。

Exception in thread "main" java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlinsorce.NonNullPropertyHolder.setNonNullProperty, parameter <set-?>
        at kotlinsorce.NonNullPropertyHolder.setNonNullProperty(NonNullPropertyHolder.kt)
        at javasorce.test.GetSetNonNullPropertyHolder.getNonNullPropertyHolderWithNullInProperty(GetSetNonNullPropertyHolder.java:14)
        at kotlinsorce.CallGetSetNonNullPropertyHolderKt.main(CallGetSetNonNullPropertyHolder.kt:8)

Kotlinで定義した非Null型のオブジェクトにJavaでnullを代入した場合、Javaコードのために自動生成されたセッター内でIllegalArgumentExceptionが発生するようになっているみたいですね。

Kotlinで定義した非Null型のオブジェクトにJavaでnullを代入した場合、コンパイル時点ではnullの代入の検出はできませんが、実行時にnullの代入を検出できるようになっているようです。
この動作からもKotlinのNull安全が働いているということがわかりますね。

詳しいことは(ここ)[https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html]に記載があります。

このように、実行時にNullPointerExceptionの代わりにIllegalArgumentExceptionが発生してしまい、ランタイムエラーとなってしまいますが、Kotlinに定義されている非Null型のプロパティにnullを代入しないようにJavaのコードを修正する必要が出てくるため、思わぬNull参照は避けられるようになっています。
この修正は一見面倒なように思えますが、後に大きな財産になる修正になると僕は思います。

KotlinのNull安全は伊達ではないですね!
引き続き、KotlinのNull安全に関する解説をしていきます。

Null可能型の安全な使い方② - ?.演算子で安全な呼び出し -

条件式を使用せずにNull可能型のオブジェクトを参照するには?.演算子を使用します。
?.演算子を使用することで安全にNull可能型のオブジェクトを参照することができます。

nullableString?.length

Null可能型のオブジェクトのメンバーなどにアクセスしたい場合に上記のように書くことで安全に参照することができます。
もしNull可能型のオブジェクト、ここではnullableStringがnullではない場合、lengthプロパティの値が返ってきます。
もしnullableStringがnullの場合、nullを返します。

?.演算子を使用する場合、以下のように処理することができます。

val nullableString: String? = "nullableString"
val length:Int? = nullableString?.length
if (length != null) {
    println("length = ${length}")
} else {
    println("length is not exist.")
}

?.演算子を使った処理は後述するエルビス演算子を使用することでより簡潔に書くことができます。

この?.演算子は、先ほど説明したプラットフォーム型のプロパティを同じ方法で安全に参照することもできます。
プラットフォーム型を使用する時は積極的に使用したほうがよさそうですね!

また、?.演算子はチェーンを使用する場合にも役に立ちます。

例えば、あるファイル管理システムがあったとします。
そのファイル管理システムではフォルダにタグの設定ができます。
そこで、あるタグが設定されたフォルダのみのリストを取得する処理を書きたいとします。

フォルダにはフォルダ情報が含まれており、その中にタグの情報が含まれており、タグ情報にはタグの重要度やタグの色、タグの名前が含まれているとします。
そのような処理をしたい場合、以下のように処理することができます。

for (currentFolder in folderList) {
    val tagName: String? = currentFolder.folderInfo?.tagInfo?.tagName
    if (tagName != null && tagName.equals(specifiedTagName)) specifiedTagFolderList.add(currentFolder)
}

この処理では、フォルダに含まれるフォルダ情報、フォルダ情報に含まれるタグ情報、タグ情報に含まれるタグ名の順に参照していき、どこかがnullなら変数tagNameにはnullが代入され、タグ名まで全てが存在した場合、タグ名が変数に代入されるようになります。

このように、チェーンを使用する場合、この?.演算子はとても役に立つものとなっています。

また、nullでない値の場合のみチェーン操作を実行したい場合、let関数を使用することで安全に処理を行うこともできます。

for (currentFolder in folderList) {
    currentFolder.folderInfo?.tagInfo?.tagName?.let {
        if (it.equals(specifiedTagName)) specifiedTagFolderList.add(currentFolder)
    }
}

let関数は渡されたラムダ式をただ単に実行する関数です。
この処理では?.演算子で各プロパティをチェーンで繋ぎ、最後のプロパティのtagNameがnullでない場合にlet関数を実行するようにしています。
そうすることにより、nullでないオブジェクトへの使用を安全に行うことができます。
letが実行された時点でtagNameがnullでないことが確定しているため、null確認もしなくてもいいというのも利点の一つです。

この処理で出てきたit演算子ですが、it演算子はラムダ式内で使用できる演算子です。
let内のit演算子はtagNameの参照がitとなっています。

it演算子に関してはここここに記載があります。

Null可能型の安全な使い方③ - エルビス演算子で安全な呼び出し -

エルビス演算子を使用することで、条件式でNull可能型を使用できなかったオブジェクトを安全に参照することができます。
エルビス演算子を使用すると、もしそのオブジェクトがnullでなければそのオブジェクトを使用し、もしnullなら非Null型の任意の値を返すような記述をすることができます。

val nullableString: String? = "nullableString"
val length = nullableString?.length ?: -1

?:がエルビス演算子になります。
エルビス演算子は以下の処理の省略系に近いです。

val nullableString: String? = "nullableString"
val length = if (nullableString != null) nullableString.length else -1

上記の処理は、条件式でnullを確認する、で説明したとおり、特定のNull可能型のオブジェクトの場合、使用することができません。
ですが、エルビス演算子と?.演算子を使用することで、全てのNull可能型のオブジェクトを安全に参照することができます。

エルビス演算子は、エルビス演算子(?:)の左側がnullでない場合、左側のオブジェクトを返します。
nullの場合、右側の式を返します。
条件式でいうifとelseの関係になりますので、式の右側は、左側がnullの場合のみ評価されることに注意してください。

エルビス演算子を使用した式では、throwやreturnをエルビス演算子の右側で使用することもできます。
例えば関数の引数の確認で有用になる可能性があります。

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("名前が必要です。")
    // ...
}

Null安全なんてどうでもいい! 俺はNullPointerExceptionを発生させたいんだ!

そんなNullPointerExceptionを愛してやまない人にうってつけの演算子がKotlinには用意されています。
それが!!演算子です。

val nullableString: String? = null
val length = nullableString!!.length

!!演算子は、もしNull可能型のオブジェクトがnullでなければそのまま値を返し、もしnullならNullPointerExceptionをthrowします。
なので、NullPointerExceptionを発生させたいなら、!!演算子を使用することでNullPointerExceptionを拾うことができます。

いくらNull安全でプログラムを組めるといっても、NullPointerExceptionを拾いたい場面はあります。
そのような場合に使用する演算子になります。

Null可能型を使用した安全なキャスト

標準のキャストは、そのオブジェクトが目的の型でない場合、ClassCastExceptionを発生させます。
標準のキャストの代わりに、as?演算子を使用して安全にキャストをすることができます。

val nullableInt: Int? = nullableObject as? Int

as?演算子を使用すると、対象のオブジェクトが目的の型にキャストできなかった場合、nullを返します。
as?演算子の機能は、C#でいうas演算子を使用したキャストと同じような機能です。

使いどころはたくさんあると思います。

Null可能型のコレクションを安全に使用する

Null可能型の要素を持つコレクションを使用していて非Nullな要素のみを取り出したい場合、filterNotNull関数を使用することによってそれを実現することができます。

val nullableList: List<int?> = listOf(1, 2, null, 4)
val intList: List<int> = nullableList.filterNotNull()

使いどころとしては、JavaでListを取得するような処理の後にfilterNotNull関数を呼び出すことによって、要素がnullかどうかを気にすることなく使用できます。

その他のNull安全な機能

上記で紹介した機能の他にNull安全な機能がKotlinには備わっています。

Kotlinでは関数の引数にもNull可能型、非Null型を指定する必要があります。
例えば以下の関数は必ずnullでないオブジェクトが来るのでnull確認することなく処理を行えます。

fun calculateStingLength(target1: String, target2: String): Int {
    return target1.length + target2.length
}

非Null型の引数のため、必ずnullではないため、安全に参照することができます。

次にプラットフォーム型を使用する時の安全な使い方です。
Javaで定義されたメソッドで返り値として設定されたプラットフォーム型はnullで返ってくる可能性があります。
そこで以下のように返り値を受け取ることで、受け取ったオブジェクトの参照に関してはNull安全になります。

val nullableHogeClass: HogeClass? = foo.getHogeClass()

// Null可能型のオブジェクトとして明示的に宣言しているため、KotlinのNull安全に則った呼び出しをしていないのでここでコンパイルエラーになる
nullableHogeClass.bar()

このように、Javaのメソッドから受け取るオブジェクトは明示的にNull可能型として宣言して受け取れば、そのオブジェクトの参照はNull安全になります。
nullが返ってくるようなメソッドを使用する場合、明示的に宣言することをおすすめします。

ただし、そのオブジェクトが持つプロパティに関してはKotlinのNull安全が働かないので注意してください。
Javaのクラスのプロパティにアクセスする際は、?.演算子を使用することをおすすめします。
そうすることにより、KotlinのNull安全が働きます。

KotlinのNull安全はすごい、だけど…

このように非Null型とNull可能型の使い分けによってKotlinではNull安全が保証されます。
nullの参照の可能性をコンパイル時点でエラーとして報告してくれることは、開発していく上で、この上なく効率がいいです。
無駄なデバッグのコストも削減できるでしょう。

それでは、KotlinのNull安全は万能なのでしょうか。
これまでのKotlinのNull安全の機能を見直すと万能のようにも感じます。

ですが、KotlinのNull安全は完全に安全というわけではありません。
それではどのような場合、Null安全でなくなるのかを見ていきましょう。

KotlinでNullPointerExceptionが発生する場面

KotlinでNullPointerExceptionが発生する可能性は以下になります。

NullPointerException()をthrowする明示的な呼び出し

先ほど説明した!!演算子を使用することでNullPointerExceptionが明示的に呼び出され、NullPointerExceptionが発生します。
!!演算子を使うということは、どこかでエラーをcatchすると思いますので、そこまで深刻なNullPointerExceptionの発生ではないですね。

外部JavaコードでのNullPointerExceptionの発生

これに関してはKotlin側からできることは少ないです。
できることといえば、Java側で定義されたメソッドの仕様を守り、引数に渡すオブジェクトができる限りnullにならないようにすることです。

Javaのメソッドの引数がStringなどの型の場合は、Kotlin側で非Null型として宣言すればNullPointerExceptionを避けることはできそうですね。

初期化に関してのデータの不一致がある(コンストラクター内のどこかで、利用可能な初期化されていないオブジェクトが使用される)

この説明だけだとわかりにくいですよね。

少し長いですが、以下にその例を記載します。
簡単に言うと、以下の処理ではコンストラクタ内で未初期化のStringのlengthプロパティを参照しようとし、NullPointerExceptionが発生してしまいます。

package kotlinsorce

/**
 * まぬけなNull参照クラス
 */
class FoolishNullReferenceClass {
    /**
     * @property 非Null型の文字列
     */
    val nonNullString: String

    /**
     * @constructor コンストラクタ内で未初期化のStringの長さを参照してしまう
     */
    init {
        printLengthOfString()
        nonNullString = "nonNullString"
    }

    /**
     * Stringの長さを表示
     */
    fun printLengthOfString() {
        println("${nonNullString.length}")
    }
}

/**
 * 不運なことにNullPointerExceptionが発生する
 */
fun main(args: Array<string>) {
    val foolishNullReferenceClass = FoolishNullReferenceClass()
}

Kotlinのクラスのプロパティは、基本的にはコンストラクタの処理が終了するまでに初期化する必要があります。
もし、コンストラクタの処理が終了するまでに初期化しなかった場合、コンパイルエラーになります。

上記の処理ではコンストラクタ内できちんとプロパティを初期化しています。
ですが、その1行前でprintLengthOfString関数が呼び出されており、関数の中では初期化する前のStringの長さを参照しています。
未初期化なため、値が入っていなく、NullPointerExceptionが発生します。

このパターンは起きなさそうで起きそうなパターンだと思いますので、このようなミスをしないように気をつけたいですね。

まとめ

かなり長くなってしまいましたが、これがKotlinのNull安全の全てです。
このようにKotlinのプログラム内では、NullPointerExceptionがほぼ発生しないような設計がされています。

コンパイル時点でnull参照のルールに引っかかると、コンパイラがエラーを発生させるのはとても魅力的な機能だと思いました。
Kotlinで開発すると思いもよらないnull参照のエラーはほぼ防がれることかと思います。

この機能だけでも、Kotlinを使用して開発するメリットがあると感じました。

今回はKotlinのNull安全についてまとめましたが、Kotlinのメリットについてまとめた記事もあります。
よろしければ合わせてご参照ください。

プログラム

Android公式言語 Kotlinとはどんな言語でどんなメリットがあるのか?! サムネイル

Android公式言語 Kotlinとはどんな言語でどんなメリットがあるのか?!

2017/10/14