Kotlinのnull安全とは、「nullが入るかもしれない型(String?)」と「nullが入らない型(String)」を型で分け、nullのまま使おうとするコードをコンパイルの段階で止める仕組みです。 null かもしれない値を使うときは、?.(安全呼び出し)、?:(エルビス演算子)、!!(非null表明)、?.let などから、目的に合うものを選びます。

この記事では、9つの書き方を「書き方・nullのときの挙動・戻り値の型」で並べた早見表を最初に置き、そのあとで1つずつコードと出力を示します。コンパイルエラー文、NullPointerException、UninitializedPropertyAccessException も実際に出して、直し方とセットで載せました。Java のメソッドを呼んだときのプラットフォーム型や、var プロパティでスマートキャストが効かない場面も扱います。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc-jvm)/ OpenJDK 21.0.10。掲載したコードはすべてこの環境でコンパイル・実行し、出力をそのまま載せています。実行コマンドは kotlinc basic.kt -include-runtime -d basic.jar && java -jar basic.jar の形です。

null安全の早見表

x の型が String? のときの挙動です。

書き方 名前 x が null のとき 戻り値の型 向いている場面
x?.length 安全呼び出し 呼び出さずに null を返す Int? null ならそのまま null でよい
x ?: "なし" エルビス演算子 右側の値を返す(return・throw も書ける) String 既定値を入れる、早めに関数を抜ける
x!! 非null表明 NullPointerException で止まる String null でないことを別の手段で保証済み
x?.let { ... } 安全呼び出し+let ブロックを実行せずに null を返す ブロックの結果の null 許容型 null でないときだけ処理したい
v as? String 安全キャスト 型が合わなければ null を返す String? 型が合うか分からない値を変換
requireNotNull(x) { "..." } 引数の検査 IllegalArgumentException で止まる String 呼び出し側から渡された値の検査
checkNotNull(x) { "..." } 状態の検査 IllegalStateException で止まる String 自分のオブジェクトの状態の検査
lateinit var x: String 遅延初期化 代入前に読むと UninitializedPropertyAccessException String あとから必ず代入するプロパティ
if (x != null) { ... } スマートキャスト ブロックに入らない ブロック内で String 複数行の処理、else も書きたい

迷ったときの順番は、「?. と ?: で書けないか」→「if や ?.let で囲めないか」→「止めるならメッセージ付きの requireNotNull / checkNotNull」→「最後に !!」 です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin null安全の使い方|?. ?: !! letの違い早見表」

まず動かす:基本の書き方をまとめて確認

次のコードは、そのまま貼り付けて動かせます。name には値があり、none は null です。

fun main() {
    val name: String? = "Kotlin"
    val none: String? = null

    // ?. 安全呼び出し
    println(name?.length)        // 6
    println(none?.length)        // null

    // ?: エルビス演算子
    println(none?.length ?: 0)   // 0
    println(none ?: "名無し")     // 名無し

    // ?.let
    name?.let { println("letの中: ${it.uppercase()}") }
    val r = none?.let { it.uppercase() }
    println(r)                   // null

    // as? 安全キャスト
    val any: Any = 123
    val s: String? = any as? String
    val i: Int? = any as? Int
    println(s)                   // null
    println(i)                   // 123

    // !! 非null表明
    println(name!!.length)       // 6
}

実行結果:

6
null
0
名無し
letの中: KOTLIN
null
null
123
6

none?.length は例外にならず null を返し、?: 0 を付けると 0 になります。?.let のブロックは、値があるときだけ動きます。 as? は型が合わないときに null を返すので、123 as? String は null、123 as? Int は 123 です。

String と String? の違い:コンパイルで止まる3つの形

Kotlin では、型名のうしろに ? を付けた型だけが null を受け取れます。

型 null を代入 メンバーを直接呼ぶ 例
String できない(コンパイルエラー) できる val a: String = "abc"
String? できる ?. などが必要 val b: String? = null

よく出会うコンパイルエラーは次の3つです。どれも実行する前に見つかるので、落ち着いて型を合わせれば解決します。

1. null かもしれない値のメンバーを呼んだ

fun main() {
    val name: String? = null
    println(name.length)
}
err1.kt:3:17: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
    println(name.length)
                ^

これが null安全でいちばんよく見るエラー文です。検索では先頭が大文字の「Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?」という表記でも見かけますが、意味は同じです。「String? 型の値には ?. か !!. でしか呼び出せません」という内容なので、name?.length に直すか、name?.length ?: 0 のように既定値まで決めます。

2. 非null型に null を入れようとした

fun main() {
    val name: String = null
    println(name)
}
err2.kt:2:24: error: null cannot be a value of a non-null type 'String'.
    val name: String = null
                       ^^^^

null を入れたいなら型を String? にします。入れたくないなら、空文字 "" などの既定値を使います。

3. String? を String の引数に渡した

fun greet(name: String) = "Hello, $name"

fun main() {
    val input: String? = readlineOrNull()
    println(greet(input))
}

fun readlineOrNull(): String? = null
err3.kt:5:19: error: argument type mismatch: actual type is 'String?', but 'String' was expected.
    println(greet(input))
                  ^^^^^

直し方は3通りあります。

直し方 書き方 null のときの動き
既定値を渡す greet(input ?: "ゲスト") "ゲスト" で呼ぶ
null なら呼ばない input?.let { greet(it) } 呼ばずに null
関数側を null 許容にする fun greet(name: String?) 関数の中で扱いを決める

安全呼び出し ?. と エルビス演算子 ?: の使い方

?. はつなげて書けます。途中のどこかが null なら、そこで止まって全体が null になります。?: は「左が null のときだけ右を使う」演算子で、右側には値だけでなく return や throw も書けます。

class Address(val city: String?)
class Member(val name: String, val address: Address?)

fun cityOf(m: Member?): String = m?.address?.city ?: "未登録"

fun shout(m: Member?): String {
    val name = m?.name ?: return "会員がいません"
    return name.uppercase()
}

fun main() {
    println(cityOf(Member("taro", Address("Tokyo"))))
    println(cityOf(Member("jiro", Address(null))))
    println(cityOf(Member("saburo", null)))
    println(cityOf(null))
    println(shout(null))
    println(shout(Member("taro", null)))

    // 安全呼び出しでの代入: 左辺が null なら右辺は評価されない
    class Box(var v: Int)
    val box: Box? = null
    box?.v = compute()
    println("代入はスキップ")

    // == は null 同士でも安全
    val a: String? = null
    println(a == null)
    println(a == "x")
    println(a.toString())      // "null" という文字列
}

fun compute(): Int { println("compute が呼ばれた"); return 1 }

実行結果:

Tokyo
未登録
未登録
未登録
会員がいません
TARO
代入はスキップ
true
false
null

ポイントは4つです。

  • m?.address?.city ?: "未登録" は、会員・住所・都市のどれが null でも「未登録」になります。4通りの入力で確かめた結果が最初の4行です。
  • val name = m?.name ?: return "..." のように、null なら関数を抜ける書き方ができます。この行より下では name は String として扱えます。
  • box?.v = compute() のように代入の左側に ?. を置くと、box が null のとき代入そのものが行われず、右側の compute() も呼ばれません(出力に「compute が呼ばれた」が出ていません)。
  • == での比較は null でも安全です。a == "x" は a が null でも例外にならず false を返します。a.toString() は null のとき "null" という文字列になります。

?.let の使い方と、?. を付けない let との違い

let は「値を it として受け取り、ブロックの結果を返す」関数です。?. と組み合わせることで「null でないときだけ実行」になります。

fun main() {
    val a: String? = null

    // ?.let は「null でないときだけ」実行
    a?.let { println("A: $it") }

    // .let(?なし)は null でも実行される。it は String? のまま
    a.let { println("B: $it") }

    // ?.let { } ?: ... の落とし穴: ブロックが null を返すと右側も動く
    val b: String? = "x"
    val r = b?.let { null } ?: "右側が動いた"
    println(r)

    // if 式なら意図どおり
    val r2 = if (b != null) b.uppercase() else "なし"
    println(r2)

    // takeIf / takeUnless と組み合わせる
    val age = 15
    println(age.takeIf { it >= 18 }?.let { "成人" } ?: "未成年")
}

実行結果:

B: null
右側が動いた
X
未成年
書き方 a が null のとき ブロック内の it の型
a?.let { ... } ブロックは動かない(A の行は出力されない) String
a.let { ... } ブロックが動く(B: null と出力された) String?

?.let { ... } ?: 既定値 は、ブロックの結果が null のときも右側が動きます。 上の例では b に値があるのに「右側が動いた」と出力されました。if-else の代わりに使うときは、ブロックが null を返さないことを確かめるか、素直に if (b != null) ... else ... と書くと意図どおりになります。

?.let が向いているのは、null でないときだけ関数に渡したい、var プロパティの値を1回だけ読んで使いたい、という場面です。1つのメンバーを呼ぶだけなら a?.length のほうが短く書けます。

安全キャスト as? の使い方

as は型が合わないと ClassCastException になりますが、as? は null を返します。?: と組み合わせると、型が合わないときの扱いを1行で書けます。

fun lengthOf(v: Any?): Int = (v as? String)?.length ?: -1

fun main() {
    println(lengthOf("abc"))
    println(lengthOf(123))
    println(lengthOf(null))
}

実行結果:

3
-1
-1

文字列なら長さ、数値や null なら -1 になりました。型で分岐したいだけなら、あとで紹介する is と when のスマートキャストのほうが読みやすくなります。

!! と requireNotNull・checkNotNull の違い

!! は「この値は null ではない」とコンパイラに伝える演算子です。null だった場合は実行時に止まります。

fun main() {
    val name: String? = null
    println(name!!.length)
}
Exception in thread "main" java.lang.NullPointerException
    at BangKt.main(bang.kt:3)
    at BangKt.main(bang.kt)

!! で止まったときの NullPointerException にはメッセージが付きません。 行番号は分かりますが、「なぜ null では困るのか」は書かれていません。そこで、止めたい場面ではメッセージを添えられる関数を使うと、あとで読む人に親切です。

class Order(val id: Int, val memo: String?)

fun main() {
    val order = Order(1, null)

    try {
        val memo: String = requireNotNull(order.memo) { "memo は必須です (id=${order.id})" }
        println(memo)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }

    try {
        val memo: String = checkNotNull(order.memo) { "memo が未設定の状態です" }
        println(memo)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }

    try {
        val memo: String = requireNotNull(order.memo)
        println(memo)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }

    try {
        val memo: String = order.memo ?: error("memo がありません")
        println(memo)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }

    try {
        val memo: String = order.memo!!
        println(memo)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }
}

実行結果:

IllegalArgumentException: memo は必須です (id=1)
IllegalStateException: memo が未設定の状態です
IllegalArgumentException: Required value was null.
IllegalStateException: memo がありません
NullPointerException: null
書き方 null のときの例外 メッセージ 使い分け
requireNotNull(x) { "..." } IllegalArgumentException 自分で書ける(省略時は Required value was null.) 引数など、外から渡された値
checkNotNull(x) { "..." } IllegalStateException 自分で書ける オブジェクトの状態(初期化済みか等)
x ?: error("...") IllegalStateException 自分で書ける 式の途中で短く書きたい
x ?: throw MyException() 好きな例外 自分で書ける 独自の例外を投げたい
x!! NullPointerException なし(null と表示) 直前で null でないと確認済みの値

requireNotNull と checkNotNull は、戻り値が非null型になります。val memo: String = requireNotNull(order.memo) のように受けられるので、そのあとは ?. なしで使えます。

!! を見かけたときの書き換え例

もとの書き方 書き換え 変わること
user!!.name user?.name ?: "名無し" 止まらずに既定値で続く
map["key"]!! map.getValue("key") キー名入りのメッセージで止まる
list.firstOrNull()!! list.first() 「List is empty.」と分かる
param!! requireNotNull(param) { "param は必須" } 理由が残る
field!!(あとで代入するプロパティ) lateinit var field: T 型が非nullになり !! が不要に

lateinit の使い方:あとから代入する非null型プロパティ

「コンストラクタでは値が決まらないが、使う前には必ず代入する」プロパティは、String? にして毎回 !! を付ける代わりに lateinit var で宣言できます。代入する前に読むと、UninitializedPropertyAccessException で止まります。

class Repo {
    lateinit var url: String

    fun isReady() = ::url.isInitialized
}

fun main() {
    val repo = Repo()
    println("初期化済み? ${repo.isReady()}")
    println(repo.url)
}
初期化済み? false
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property url has not been initialized
    at Repo.getUrl(late.kt:2)
    at LateKt.main(late.kt:10)
    at LateKt.main(late.kt)

メッセージにプロパティ名(url)が入るので、どの初期化が抜けているかがすぐ分かります。対処は「読む前に代入する」か、「::url.isInitialized で確かめてから読む」です。

class Repo {
    lateinit var url: String
    val isReady: Boolean get() = ::url.isInitialized
}

class Config {
    val text: String by lazy {
        println("  (lazy: 初回だけ計算)")
        "loaded"
    }
}

fun main() {
    val repo = Repo()
    println("初期化前: ${repo.isReady}")
    repo.url = "https://example.com"
    println("初期化後: ${repo.isReady} / ${repo.url}")

    val c = Config()
    println(c.text)
    println(c.text)
}

実行結果:

初期化前: false
初期化後: true / https://example.com
  (lazy: 初回だけ計算)
loaded
loaded

初回に読むときに値を計算すればよい場合は by lazy が合います。val のまま使え、計算は1回だけです(出力の「(lazy: 初回だけ計算)」が1度しか出ていません)。

lateinit に付けられる型の条件

Int などの基本型、null 許容型、val には付けられません。実際にコンパイルするとこうなります。

class A {
    lateinit var count: Int
    lateinit var memo: String?
    lateinit val name: String
}
fun main() {}
late3.kt:2:5: error: 'lateinit' modifier is not allowed on properties of primitive types.
    lateinit var count: Int
    ^^^^^^^^
late3.kt:3:5: error: 'lateinit' modifier is not allowed on properties of a type with nullable upper bound.
    lateinit var memo: String?
    ^^^^^^^^
late3.kt:4:5: error: 'lateinit' modifier is allowed only on mutable properties.
    lateinit val name: String
    ^^^^^^^^
やりたいこと 選ぶもの
あとから代入する、型は String やクラス lateinit var
あとから代入する、型は Int や Boolean var count: Int = 0 のように既定値、または Int?
最初に読むときに計算する val x by lazy { ... }
値がないことも正常な状態 var x: String? = null

公式ドキュメントでは、isInitialized を使えるのは、そのプロパティに書いている場所からアクセスできる場合(同じクラス、外側のクラス、同じファイルのトップレベル)と説明されています。

スマートキャストの使い方と、var プロパティで効かない理由

if (x != null) や is で確かめたあと、コンパイラはその変数を自動で非null型(または確かめた型)として扱います。これがスマートキャストです。

fun describe(x: Any?): String {
    if (x == null) return "null です"
    // ここから下では x は Any(非null)
    if (x is String && x.isNotEmpty()) return "文字列 ${x.length} 文字"
    return when (x) {
        is Int -> "整数 ${x + 1}"
        is List<*> -> "リスト ${x.size} 件"
        else -> "その他"
    }
}

fun main() {
    println(describe(null))
    println(describe("abc"))
    println(describe(41))
    println(describe(listOf(1, 2)))

    // Kotlin 2.x: 条件を変数に入れてもスマートキャストが効く
    val s: String? = "kotlin"
    val ok = s != null
    if (ok) println(s.length)
}

実行結果:

null です
文字列 3 文字
整数 42
リスト 2 件
6

if (x == null) return のあとは x が非null として扱われ、x is String && x.isNotEmpty() のように && の右側でも効きます。最後の例のように、条件を変数 ok に入れてから if で使う形でも、Kotlin 2.x ではスマートキャストが効きます。

var プロパティではコンパイルエラーになる

class User(var nickname: String?)

fun main() {
    val user = User("taro")
    if (user.nickname != null) {
        println(user.nickname.length)
    }
}
smart.kt:6:17: error: smart cast to 'String' is impossible, because 'nickname' is a mutable property that could be mutated concurrently.
        println(user.nickname.length)
                ^^^^^^^^^^^^^

メッセージは「nickname は変更できるプロパティで、同時に書き換えられる可能性があるため、String へのスマートキャストができません」という意味です。null チェックと .length の間に、別の処理が nickname を null に変えるかもしれない、とコンパイラは考えます。

直し方は3通りあります。いったんローカルの val に取り出すのが基本です。

class User(var nickname: String?, val email: String?)

fun main() {
    val user = User("taro", "taro@example.com")

    // val プロパティ(同じモジュール・独自getterなし)はスマートキャストが効く
    if (user.email != null) {
        println(user.email.length)
    }

    // 直し方1: ローカルの val に取り出す
    val nick = user.nickname
    if (nick != null) {
        println(nick.length)
    }

    // 直し方2: ?.let
    user.nickname?.let { println(it.length) }

    // 直し方3: ?: で早めに抜ける
    val n = user.nickname ?: return
    println(n.length)
}

実行結果:

16
4
4
4

公式ドキュメントの「Smart cast prerequisites」では、スマートキャストが使える条件が次のように整理されています。

変数の種類 スマートキャスト
val のローカル変数 常に使える(委譲されたローカル変数を除く)
val のプロパティ private・internal、または同じモジュール内での確認なら使える。open や独自 getter があるものは対象外
var のローカル変数 確認から使用までの間に変更がなく、変更するラムダに捕まっていなければ使える
var のプロパティ 使えない

Java 由来のプラットフォーム型で NullPointerException が出る例

Kotlin のコードだけなら、null は型で管理できます。気を付けたいのは Java のメソッドを呼ぶときです。Java の戻り値は「null かもしれないし、そうでないかもしれない」ので、Kotlin はこれをプラットフォーム型として扱い、エラーメッセージなどでは String! と表記します。プラットフォーム型は String としても String? としても受け取れ、コンパイルエラーにはなりません。

System.getProperty は Java 標準のメソッドで、キーがないと null を返します。

fun main() {
    // System.getProperty は Java のメソッド。戻り値はプラットフォーム型 String!
    val p = System.getProperty("no.such.key")
    println("p = $p")

    // (1) null許容型で受ける → コンパイラが ?. を求めてくれる
    val safe: String? = System.getProperty("no.such.key")
    println(safe?.length ?: -1)

    // (2) そのままメンバーを呼ぶ → コンパイルは通り、実行時に止まる
    try {
        println(p.length)
    } catch (e: NullPointerException) {
        println("(2) NPE: ${e.message}")
    }

    // (3) 非null型で受ける → 代入の時点で止まる
    try {
        val s: String = System.getProperty("no.such.key")
        println(s)
    } catch (e: NullPointerException) {
        println("(3) NPE: ${e.message}")
    }
}

実行結果:

p = null
-1
(2) NPE: Cannot invoke "String.length()" because "p" is null
(3) NPE: getProperty(...) must not be null
受け方 コンパイル 実行時
val safe: String? = ... 通る。以降は ?. が必要になる 安全に進む
型を書かずに受けて p.length 通る メンバーを呼んだ行で NullPointerException
val s: String = ... 通る 代入の行で NullPointerException(must not be null)

(3) のメッセージ「getProperty(...) must not be null」は、Kotlin コンパイラが代入のところに入れた検査によるものです。null が先へ広がる前に止めてくれます。

自作の Java クラスでも同じ

Java 側にアノテーションがないメソッドを用意し、Kotlin から呼びます。

public class Legacy {
    // アノテーションなし → Kotlin からは String!(プラットフォーム型)
    public static String findName(int id) {
        return id == 1 ? "taro" : null;
    }
}
fun main() {
    val a = Legacy.findName(1)
    println(a.length)

    val b = Legacy.findName(2)
    println(b?.length)          // null許容として扱えば安全
    println(b.length)           // 非nullとして扱うと実行時に止まる
}

kotlinc Main.kt Legacy.java -d out と javac -d out Legacy.java でコンパイルして実行した結果です。

4
null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "b" is null
    at MainKt.main(Main.kt:7)
    at MainKt.main(Main.kt)

findName(1) は値があるので 4、findName(2) は null なので b?.length は null、b.length は NullPointerException になりました。

対処は、Java の戻り値を受ける時点で型を明示することです。 null が返る可能性があるなら val b: String? = Legacy.findName(2) と書けば、そのあとの b.length はコンパイルエラーになり、?. や ?: を書くよう促されます。Java 側のソースを直せる場合は、null 可否のアノテーションを付けると Kotlin が型を判断できます。公式ドキュメントには、JetBrains(org.jetbrains.annotations の @Nullable・@NotNull)、JSpecify(org.jspecify.annotations)、JSR-305(javax.annotation)、Lombok(lombok.NonNull)などのアノテーションに対応していると書かれています。

コレクションと null:どこに ? が付いているかを見る

List<String?> と List<String>? は別物です。前者は「要素が null かもしれない」、後者は「リスト自体が null かもしれない」です。

fun main() {
    val a: List<String?> = listOf("a", null, "c")   // 要素が null かもしれない
    val b: List<String>? = null                       // リスト自体が null かもしれない

    println(a.filterNotNull())            // [a, c]
    println(a.mapNotNull { it?.uppercase() })
    println(b.orEmpty().size)             // 0
    println(b?.size ?: 0)                 // 0

    val s: String? = null
    println(s.isNullOrEmpty())            // true
    println(s.isNullOrBlank())            // true
    println(s.orEmpty().length)           // 0

    val m = mapOf("a" to 1)
    val v: Int? = m["zzz"]
    println(v)                            // null
    println(m.getOrDefault("zzz", 0))     // 0
    try { m.getValue("zzz") } catch (e: NoSuchElementException) { println(e.message) }

    println("12x".toIntOrNull())          // null
    println(listOf<Int>().firstOrNull())  // null
    println(listOf(1, 2).getOrNull(5))    // null
}

実行結果:

[a, c]
[A, C]
0
0
true
true
0
null
0
Key zzz is missing in the map.
null
null
null
やりたいこと 書き方 null・該当なしのとき
null の要素を除く list.filterNotNull() 除いたリスト
変換しつつ null を除く list.mapNotNull { ... } 除いたリスト
null のリストを空として扱う list.orEmpty() 空リスト
null か空かをまとめて判定 s.isNullOrEmpty() / s.isNullOrBlank() true
Map から取り出す map[key] null
Map から既定値つきで取り出す map.getOrDefault(key, 0) 既定値
数値に変換 "12x".toIntOrNull() null
先頭の要素 list.firstOrNull() null
位置を指定して取り出す list.getOrNull(5) null

標準ライブラリには「〜OrNull」という名前の関数がそろっています。 例外で止まる版(first()、toInt()、getValue())と、null を返す版(firstOrNull()、toIntOrNull()、[])を使い分けると、?: と組み合わせて既定値を1行で書けます。

エラー文から引く対処表

出たメッセージ いつ出るか 原因 対処
only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?' コンパイル時 null 許容型のメンバーを . で呼んだ ?. にする、?: で既定値、if で囲む
null cannot be a value of a non-null type 'String' コンパイル時 非null型に null を代入 型を String? にする、既定値を入れる
argument type mismatch: actual type is 'String?', but 'String' was expected コンパイル時 null 許容型を非null型の引数に渡した ?: で既定値、?.let で囲む
smart cast to 'String' is impossible, because '...' is a mutable property that could be mutated concurrently コンパイル時 var プロパティを null チェック後にそのまま使った ローカルの val に取り出す、?.let
'lateinit' modifier is not allowed on properties of primitive types コンパイル時 Int などに lateinit を付けた 既定値を入れる、by lazy
java.lang.NullPointerException(メッセージなし) 実行時 !! を付けた値が null ?:、requireNotNull などに置き換える
NullPointerException: ... must not be null 実行時 Java の戻り値(null)を非null型で受けた String? で受ける
NullPointerException: Cannot invoke "String.length()" because "p" is null 実行時 プラットフォーム型のままメンバーを呼んだ 受ける時点で型を明示する
UninitializedPropertyAccessException: lateinit property url has not been initialized 実行時 lateinit のプロパティを代入前に読んだ 先に代入する、isInitialized で確認
IllegalArgumentException: Required value was null. 実行時 requireNotNull に null が渡った 呼び出し側の値を見直す

公式ドキュメントでは、Kotlin で NullPointerException が起きる原因として、明示的な throw NullPointerException()、!! の使用、初期化中のデータの不整合(コンストラクタで初期化前の this が外に渡る場合など)、Java との相互運用の4つが挙げられています。ふだんのコードで出会うのは、ほぼ !! と Java 連携の2つです。

どれを選ぶか:場面別の判断表

場面 選ぶ書き方 例
null なら null のままでよい ?. user?.email
null のときの値が決まっている ?: name ?: "ゲスト"
null なら処理を打ち切る ?: return val u = find(id) ?: return
null でないときだけ関数に渡す ?.let email?.let { send(it) }
null のときと、そうでないときで処理が分かれる if (x != null) ... else ... 複数行の処理
型が合うときだけ使う as? または is (v as? String)?.length
引数が null なら呼び出し側の誤り requireNotNull 関数の先頭で検査
状態が整っていないなら手順の誤り checkNotNull 接続済みかの検査
あとから必ず代入するプロパティ lateinit var 初期化メソッドで代入
最初に読むとき計算する by lazy 設定値の読み込み
Java のメソッドの戻り値 型を明示して受ける val s: String? = javaObj.name()

チェックリスト

  • [ ] 型に ? を付けるのは、null が正常な値として意味を持つときだけにしている
  • [ ] !! を書く前に、?:・?.let・requireNotNull で書けないか確かめた
  • [ ] var プロパティは、ローカルの val に取り出してから null チェックしている
  • [ ] Java のメソッドの戻り値は、型を明示して受けている
  • [ ] ?.let { } ?: ... のブロックが null を返さないことを確かめた
  • [ ] 止める場面では、理由が分かるメッセージを添えている

よくある質問

Q. ?. と !! はどちらを使えばよいですか? A. 基本は ?. です。?. は null のとき null を返して処理が続き、!! は NullPointerException で止まります。null のときにどうしたいかが決まっているなら ?: で既定値や return を書き、null であってはならない値なら requireNotNull でメッセージを添えると、原因が伝わりやすくなります。

Q. ?.let と if (x != null) はどう使い分けますか? A. null でないときに1つの処理をするだけなら ?.let、else も書きたいときや複数行になるときは if が読みやすくなります。var プロパティは if ではスマートキャストが効かないので、?.let か、ローカルの val に取り出す書き方を使います。

Q. Kotlin なのに NullPointerException が出るのはなぜですか? A. 多くは !! を付けた値が null だった場合か、Java のメソッドが null を返した場合です。スタックトレースの行に !! があれば前者、Java の API を呼んでいれば後者です。後者は、戻り値を String? のように null 許容型で受けると、コンパイラが ?. を求めてくれるようになります。

Q. lateinit と String? はどちらがよいですか? A. 「使う時点では必ず値が入っている」なら lateinit、「値がないことも正常な状態」なら String? です。lateinit は読むたびに ?. や !! を書かずに済み、初期化が抜けたときはプロパティ名入りのメッセージで知らせてくれます。

まとめ

  • null を入れられるのは ? の付いた型だけ。null かもしれない値をそのまま使うコードは、コンパイルで止まる
  • ?. は null なら null、?: は null なら右側、!! は null なら NullPointerException
  • ?.let は null でないときだけブロックを実行。?.let { } ?: ... はブロックが null を返すと右側も動く
  • 止めるなら requireNotNull(引数)と checkNotNull(状態)。メッセージを添えられ、戻り値は非null型になる
  • lateinit var は代入前に読むと UninitializedPropertyAccessException。::x.isInitialized で確認できる
  • var プロパティはスマートキャストが効かない。ローカルの val に取り出すか ?.let を使う
  • Java のメソッドの戻り値はプラットフォーム型。型を明示して受けると、null の扱いをコンパイラが確認してくれる

参考・出典

  • Kotlin 公式ドキュメント「Null safety」 https://kotlinlang.org/docs/null-safety.html
  • Kotlin 公式ドキュメント「Calling Java from Kotlin」(Null-safety and platform types) https://kotlinlang.org/docs/java-interop.html
  • Kotlin 公式ドキュメント「Type checks and casts」(Smart cast prerequisites) https://kotlinlang.org/docs/typecasts.html
  • Kotlin 公式ドキュメント「Properties」(Late-initialized properties and variables) https://kotlinlang.org/docs/properties.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin null安全の使い方|?. ?: !! letの違い早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?