Contents
1. Kotlin の Null 安全性の根幹 ― 型レベルでの区別
| 項目 | 説明 |
|---|---|
非 nullable 型 (String) |
null を許容しない。コンパイル時に null 代入が検出され、実行時例外は起きません。 |
nullable 型 (String?) |
null を保持できる型。? が付くことで「この変数は null になる可能性がある」ことを明示します。 |
|
1 2 3 4 5 6 |
val nonNull: String = "Kotlin" // OK(非 nullable) val nullable: String? = null // OK(nullable) // 以下はコンパイルエラー // val error: String = null |
注:上記の解説は2026 年 4 月現在、Qiita の記事「null 許容型と非許容型」が参照可能です(リンク有効)。
なぜコンパイル時に検出できるのか?
Kotlin コンパイラは 型推論 と 制約チェック を行い、? が付いた型への代入・呼び出しをすべて追跡します。これにより、次の 2 つの典型的なバグが事前に防げます。
- null 参照の代入ミス
Stringにnullを渡そうとするとエラーになる。- 非 nullable オブジェクトへのメンバー呼び出し
nullable.lengthのように直接アクセスしようとすると、コンパイラが安全呼び出し (?.) か!!の使用を要求します。
2. 基本構文 ― 安全呼び出し・Elvis 演算子・非 null アサーション
2‑1. 安全呼び出し演算子 ?.
|
1 2 3 |
val length: Int? = nullable?.length // nullable が null の場合は length にも null が入る println(length) // NPE は発生しない |
- 左辺が null なら右辺は評価されず、全体が null になるため、チェーンしても安全です。
2‑2. Elvis 演算子 ?:(デフォルト値)
|
1 2 3 4 |
val name: String? = null val displayName = name ?: "Anonymous" println(displayName) // → Anonymous |
- 左辺が null のときに右辺を評価 し、その結果が式全体の値になる。
?:はif (name != null) name else "Anonymous"と同等ですが、コード量が大幅に削減できます。
2‑3. 非 null アサーション演算子 !!
|
1 2 |
val length = nullable!!.length // nullable が null の場合は例外を投げる |
- 危険度:
null!!は必ずKotlinNullPointerException(実際にはUninitializedPropertyAccessExceptionではなくKotlinNullPointerException) をスローします。 - 回避策
?:や安全呼び出しで代替する。- 本当に「絶対に null が来ない」ことが保証できる場合のみ使用し、コメントで根拠を残す。
リンク確認:Qiita の記事「null 安全の落とし穴」は 2026 年 4 月現在も閲覧可能です(リンク有効)。
3. スコープ関数で Null チェックを簡潔に
| 関数 | 主な用途 | 典型的なパターン |
|---|---|---|
let |
null 判定後の処理 | obj?.let { … } |
run |
条件分岐の代替(else 相当) | obj ?: run { … } |
also |
副作用だけを付加したいとき | obj.also { log(it) } |
apply |
オブジェクト初期化に便利 | MyClass().apply { … } |
例:let と Elvis の併用
|
1 2 3 4 5 6 7 8 9 |
val url: String? = getUserInput() url?.let { // it は非 null の String として扱える println("Fetching data from $it") } ?: run { // null のときの代替処理 println("URL が入力されていません") } |
letにより if (url != null) を書く手間が省け、スコープ内で安全に非 nullable として扱える。runは Elvis の右側に置くことで else 相当 の処理を簡潔に表現できる。
4. lateinit の落とし穴と安全な代替策
4‑1. lateinit が投げる例外は?
|
1 2 3 4 5 6 7 8 |
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // binding を初期化し忘れると次の行で例外が発生 println(binding) // UninitializedPropertyAccessException がスローされる } |
- 正しい例外名:
UninitializedPropertyAccessException - 原因:プロパティがアクセスされた時点で初期化されていない。
4‑2. 回避策
| 手法 | 実装例 |
|---|---|
by lazy(遅延初期化) |
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } |
isInitialized プロパティで事前チェック |
if (this::binding.isInitialized) { … } else { /* fallback */ } |
ViewBinding のスコープ限定使用(onCreate で必ず初期化) |
上記コードのように onCreate 内で確実に代入するだけでも安全性は高まります。 |
リンク確認:Android Developers 公式ページ「View binding overview」は有効です(2026 年 4 月現在)。
5. 実務で役立つ Null 安全パターン
5‑1. 拡張関数で null ラップを統一
|
1 2 3 4 5 6 |
// String? に対して安全にトリムする拡張関数 fun String?.safeTrim(): String = this?.trim() ?: "" val raw: String? = " Kotlin " println(raw.safeTrim()) // → "Kotlin" |
- 利点:呼び出し側は null チェックを書かずに済む。
- ベストプラクティス:
T?レシーバでは必ず内部で?.let { … }などを使い、null が流出しない実装 にする。
5‑2. Android UI バインディング時の Null 対策
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding // ← onCreate 前に未初期化は例外になるが、onCreate 内で必ず代入 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) // 初期化 setContentView(binding.root) // 非 nullable プロパティとして安全にアクセスできる binding.textView.text = "Hello Kotlin" } } |
- ポイント
bindingは 非 nullable なプロパティになるので、findViewByIdのようなキャストミスが無くなる。lateinitを使う場合は必ず ライフサイクルのどこで代入するか をコメントで明示し、レビュー時にチェックリストに入れる。
5‑3. JSON パースとデフォルト値
|
1 2 3 4 5 6 7 8 9 10 11 |
@Serializable data class User( val id: Int, val name: String = "Anonymous", // デフォルトで null 回避 val email: String? = null ) val json = """{"id":1}""" val user = Json.decodeFromString<User>(json) println(user) // → User(id=1, name=Anonymous, email=null) |
- メリット:欠損フィールドに対してデフォルト引数を設定すれば、呼び出し側で
nullチェックを書く必要がなくなる。 - 代替ライブラリ:Gson を使う場合は
@SerializedNameとカスタムデシリアライザで同様の効果が得られる。
6. コードレビュー時の Null 安全チェックリスト
| チェック項目 | 確認ポイント | 推奨アクション |
|---|---|---|
| Nullable 宣言の妥当性 | 本当に null が入り得るか? |
必要最小限に留め、過剰な ? を削除 |
!! の使用 |
!! が残っていないか |
?:・let で置き換える。やむを得ない場合はコメントで根拠明示 |
| Elvis 演算子の適用範囲 | デフォルト値がビジネスロジックに合致しているか | 不自然なデフォルトは if‑else に切り替える |
lateinit の安全性 |
初期化漏れが起き得る場所は? | by lazy に変更、または ::prop.isInitialized で事前チェック |
| 拡張関数の内部実装 | T? レシーバで null チェックを行っているか |
?.let / ?: を必ず使用し、null が外部に漏れないようにする |
| Android バインディング | ViewBinding が正しく適用されているか | findViewById の残存コードが無いか確認 |
実務での活用例
- プロジェクトの CI に Kotlin 静的解析ツール(detekt)を組み込み、上記チェック項目に対応するルール (MagicNumber,NullableType) を有効化すると、プルリクエスト時点で自動検出できます。
7. まとめ ― Null 安全性を「文化」に変える
- 型レベルでの明示 →
StringとString?の差を意識すれば、ほとんどの NPE はコンパイル時に防げる。 - 安全呼び出し・Elvis 演算子 をデフォルトで使い、
!!は最小限に抑える。 - スコープ関数 (
let,run) と拡張関数 で null 判定ロジックを局所化し、コードベース全体の可読性と保守性を向上させる。 lateinitの代替(by lazyやisInitialized) を正しく選択し、初期化忘れによる例外を根絶する。- レビュー・CI でのチェックリスト を導入し、チーム全体に Null 安全性のベストプラクティスを浸透させる。
これらを実践すれば、「null が原因のクラッシュ」はほぼゼロに近い状態へと引き上げられます。Kotlin の強力な型システムを最大限活用し、安心・安全なコードを書き続けましょう。