Kotlin null安全の使い方|?. ?: !! letの違い早見表
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期連続の黒字決算。