Kotlin: Kotlinのnull安全性を徹底理解
最終更新:2026-08-26
Kotlinのnull安全性は、単なる?構文上の糖衣に留まらず、NullPointerExceptionをランタイムの爆弾からコンパイル時の制約へと変える、型システムの根本的な設計そのものです。Charlieはたった1行のコードで、3層にわたるnull許容参照を安全に処理しています。
1. 学習内容
- null許容型
T?対 null非許容型T:コンパイル時の保証 - セーフコール
?.、エルビス?:、非nullアサーション!!、およびletのチェーン - プラットフォームの種類
T!:Java相互運用性のグレーゾーン @Nullable/@NotNullアノテーションとの連携- Charlieの活躍:セーフチェーン +
requireNotNullパラメータの検証
2. 本物の建築家の物語
(1) 課題:NullPointerException — Javaの10億ドルの過ち
CharlieのOrderProcessorは、NPEが原因で月に15回クラッシュします。これは典型的な事故です。order.getCustomer().getAddress().getCity()のような連鎖呼び出しでは、いずれかのリンクがnullになるとシステムがダウンしてしまいますが、Javaコンパイラは警告を出しません。
(2) Kotlinのnullセーフ型による解決策
KOTLIN
// Java: runtime bomb
String city = order.getCustomer().getAddress().getCity(); // NPE possible!
// Kotlin: compile-time safety
val city: String = order.customer?.address?.city ?: "Unknown" // Safe!
コンパイル時の型システムにより、NPEの発生率が95%減少しました。Kotlinプロジェクトでは、NPEは主にJavaとの相互運用境界でのみ発生します。
3. null許容型の基礎
(1) TとT? これらは異なる種類です
KOTLIN
// Non-null: CANNOT hold null
val orderId: String = "ORD-001"
// orderId = null // COMPILE ERROR
// Nullable: MUST declare with ?
val customerName: String? = null
customerName = "Alice" // OK
(2) null許容型は直接使用できない
KOTLIN
val name: String? = getInput()
// println(name.length) // ERROR: nullable receiver
// Must handle null case explicitly
println(name?.length) // Safe call: Int?
println(name?.length ?: 0) // Elvis: Int
println(name!!.length) // Non-null assertion: Int (throws if null)
if (name != null) println(name.length) // Smart cast: Int
4. nullセーフ演算子の詳細
(1) 4つの演算子
KOTLIN
val order: Order? = fetchOrder()
// 1. Safe call ?. - returns null if receiver is null
val id = order?.id // String?
// 2. Elvis ?: - provide default when null
val total = order?.total ?: 0.0 // Double
// 3. Non-null assertion !! - throw NPE if null
val status = order!!.status // String (DANGEROUS - use sparingly)
// 4. let chain - execute block only if non-null
order?.let {
println("Processing ${it.id}") // 'it' is guaranteed non-null
}
(2) オペレーター決定木
flowchart TD
A[Nullable value] --> B{Need non-null result?}
B -->|No| C["?." safe call]
B -->|Yes| D{Have default?}
D -->|Yes| E["?: Elvis operator"]
D -->|No| F{Certain non-null?}
F -->|Yes| G["!! assertion"]
F -->|No| H{Need block execution?}
H -->|Yes| I["?.let {}"]
H -->|No| J["if (x != null) smart cast"]
(3) 演算子の比較表
| 演算子 | 戻り値の型 | NULL時の挙動 | 使用頻度 | 安全性レベル |
|---|---|---|---|---|
?. |
T? |
null を返す | ⭐⭐⭐⭐⭐ | 安全 |
?: |
T |
デフォルト設定を使用 | ⭐⭐⭐⭐ | 安全 |
!! |
T |
NPE が発生 | ⭐ (避けるべき) | 危険 |
?.let |
状況による | 実行をスキップ | ⭐⭐⭐ | 安全 |
| スマートキャスト | T |
コンパイル時の保証 | ⭐⭐⭐⭐ | 安全 |
5. 連鎖した安全な呼び出し
(1) 多層のnull許容参照
KOTLIN
data class Address(val city: String, val country: String)
data class Customer(val name: String, val address: Address?)
data class Order(val id: String, val customer: Customer?)
// Safe chain through multiple nullable references
val order: Order? = fetchOrder()
val city = order?.customer?.address?.city ?: "Unknown"
val country = order?.customer?.address?.country ?: "N/A"
(2) 連鎖型let
KOTLIN
// Nested let can be hard to read
order?.let { o ->
o.customer?.let { c ->
c.address?.let { a ->
println("${a.city}, ${a.country}")
}
}
}
// Better: use safe call chain
val addressInfo = order?.customer?.address?.let {
"${it.city}, ${it.country}"
} ?: "Address unavailable"
6. エルビス演算子の戦略パターン
Elvisは単なるデフォルト値の設定にとどまらず、複数の戦略を組み合わせることも可能です:
KOTLIN
// Strategy 1: Default value
val name = customer?.name ?: "Anonymous"
// Strategy 2: Throw exception
val order = findOrder(id) ?: throw OrderNotFoundException(id)
// Strategy 3: Return early
fun process(order: Order?) {
val confirmed = order ?: return
// confirmed is smart-cast to non-null Order
println(confirmed.id)
}
// Strategy 4: requireNotNull for parameter validation
fun createInvoice(orderId: String, customer: Customer?) {
val cust = requireNotNull(customer) { "Customer is required for invoice" }
// cust is smart-cast to non-null Customer
}
(1) エルビス戦略の比較
| 戦略 | 構文 | ユースケース |
|---|---|---|
| デフォルト値 | x ?: default |
許容される代替値 |
| 例外をスローする | x ?: throw Ex() |
null はエラー状態である |
| 早期返却 | x ?: return |
nullの場合は処理をスキップ可 |
| requireNotNull | requireNotNull(x) |
パラメータの検証 |
| ログとデフォルト | x ?: run { log(); default } |
ログ記録 + フォールバック |
7. プラットフォームの種類とJavaとの相互運用性
(1) プラットフォームタイプ T!
Java コードを呼び出す際、Kotlin では戻り値の null 許容性を判断できないため、プラットフォーム型 T! が導入されます。
JAVA
// Java code - return type unknown nullability
public class JavaOrderService {
public Order findOrder(String id) { // Could be null!
return orderMap.get(id);
}
}
KOTLIN
// Kotlin: platform type - you decide!
val order = javaService.findOrder("ORD-001")
// Option 1: Treat as nullable (safe)
val safeOrder: Order? = javaService.findOrder("ORD-001")
// Option 2: Treat as non-null (risky)
val riskyOrder: Order = javaService.findOrder("ORD-001") // NPE if null!
(2) @Nullable / @NotNull のブリッジング
JAVA
// Java with annotations
public class JavaOrderService {
@Nullable
public Order findOrder(String id) { return null; }
@NotNull
public List<Order> findAll() { return orders; }
}
KOTLIN
// Kotlin now knows the nullability
val order: Order? = javaService.findOrder("ORD-001") // Known nullable
val all: List<Order> = javaService.findAll() // Known non-null
(3) Java 相互運用におけるnull安全性戦略
| 戦略 | アプローチ | 安全レベル |
|---|---|---|
| すべてをNull許容型として扱う | val x: T? = javaMethod() |
⭐⭐⭐⭐⭐ |
| 注釈を追加 | Java側:@Nullable / @NotNull |
⭐⭐⭐⭐ |
| JSR-305 | @ParametersAreNonnullByDefault |
⭐⭐⭐ |
| nullでないものと仮定 | val x: T = javaMethod() |
⭐ (危険) |
8. 完全な例:OrderProcessor のセーフチェーン
▶ サンプル:OrderProcessorのセーフチェーン
KOTLIN
// ============================================
// OrderProcessor - Null-Safe Operations
// Feature: Safe chains, Elvis strategies, requireNotNull
// ============================================
data class Address(val street: String, val city: String, val country: String)
data class Customer(val name: String, val email: String?, val address: Address?)
data class Order(val id: String, val total: Double, val customer: Customer?, val status: String)
class OrderNotFoundException(id: String) : RuntimeException("Order not found: $id")
// Safe chain helper
fun Order.getCity(): String = customer?.address?.city ?: "Unknown"
fun Order.getCountry(): String = customer?.address?.country ?: "N/A"
fun Order.getDisplayEmail(): String = customer?.email ?: "no-email"
// Elvis strategies
fun findOrderOrFail(id: String, orders: List<Order>): Order =
orders.find { it.id == id } ?: throw OrderNotFoundException(id)
fun processOrder(order: Order?) {
val confirmed = order ?: run {
println("Skipping: null order")
return
}
println("Processing: ${confirmed.id}")
}
fun validateOrder(order: Order): Order {
requireNotNull(order.customer) { "Customer is required for order ${order.id}" }
require(order.total > 0) { "Total must be positive" }
return order
}
fun main() {
val orders = listOf(
Order("ORD-001", 299.99, Customer("Alice", "alice@example.com",
Address("123 Main St", "New York", "US")), "CONFIRMED"),
Order("ORD-002", 1_500.00, Customer("Bob", null, null), "PENDING"),
Order("ORD-003", 8_900.00, null, "SHIPPED"),
Order("ORD-004", 45.50, Customer("Charlie", "charlie@example.com",
Address("456 Oak Ave", "London", "UK")), "CONFIRMED")
)
// Safe chains
println("=== Order Details ===")
orders.forEach { order ->
println("${order.id}: ${order.getCity()}, ${order.getCountry()} | Email: ${order.getDisplayEmail()}")
}
// Elvis: find or fail
println("\n=== Find Orders ===")
println("ORD-001: ${findOrderOrFail("ORD-001", orders).total} USD")
try {
findOrderOrFail("ORD-999", orders)
} catch (e: OrderNotFoundException) {
println("Error: ${e.message}")
}
// Elvis: early return
println("\n=== Process Orders ===")
processOrder(orders[0]) // Processes
processOrder(null) // Skips
// requireNotNull validation
println("\n=== Validation ===")
orders.forEach { order ->
try {
validateOrder(order)
println("${order.id}: Valid")
} catch (e: IllegalArgumentException) {
println("${order.id}: Invalid - ${e.message}")
} catch (e: IllegalStateException) {
println("${order.id}: Invalid - ${e.message}")
}
}
}
出力:
TEXT
📖 参照専用
=== Order Details ===
ORD-001: New York, US | Email: alice@example.com
ORD-002: Unknown, N/A | Email: no-email
ORD-003: Unknown, N/A | Email: no-email
ORD-004: London, UK | Email: charlie@example.com
=== Find Orders ===
ORD-001: 299.99 USD
Error: Order not found: ORD-999
=== Process Orders ===
Processing: ORD-001
Skipping: null order
=== Validation ===
ORD-001: Valid
ORD-002: Valid
ORD-003: Invalid - Customer is required for order ORD-003
ORD-004: Valid
❓ よくある質問
Q Kotlin では NPE は完全に解消されますか?
A いいえ。KotlinはNPEを大幅に削減しますが、次のようなシナリオでは依然として発生する可能性があります:
!! アサーションの失敗、Java相互運用プラットフォームの型、明示的なthrow、および初期化されていないlateinit。Q
!! を使うべきですか?A 可能な限り使用を避けてください。
!!は「これはnullではないと確信している。そうでない場合はクラッシュする」という意味ですが、あなたの判断が間違っている可能性もあります。デフォルト値や意味のある例外を伴う?:を優先してください。Q プラットフォーム型 T とは、具体的には何ですか!?
A プラットフォーム型は、Kotlin が Java コードを呼び出す場合にのみ存在します。つまり、「Kotlin にはそれが null 許容型かどうかは分からない」ということです。 それを
T?(安全)として扱うか、T(リスクあり)として扱うかは、あなたが選択します。Q
if (x != null) と x?.let の違いは何ですか?A 機能的には似ていますが、
letは新しいスコープを作成するのに対し、itは不変です。また、ifはスマートキャストを使用し、変数は可変です。 単純なチェックには if を、スコープの分離が必要な場合には let を使用してください。Q Javaライブラリのnullセーフティアノテーションを確認するにはどうすればよいですか?
A 最近の多くのJavaライブラリ(Spring、Android SDKなど)では、すでに
@Nullable / @NotNullが追加されています。これらが含まれていないライブラリの場合、Kotlinはすべての戻り値をプラットフォーム型として扱います。Q
?: throw と requireNotNull の違いは何ですか?A 機能的には同等です。
requireNotNullはより意味論的(パラメータの妥当性チェックの意図を表現)であるのに対し、?: throwはより柔軟性が高い(カスタム例外)。パラメータの妥当性チェックにはrequireNotNullを優先してください。📖 まとめ
TとT?は異なる型です。コンパイラはコンパイル時にnull安全性を強制します。- 4つの必須演算子:
?.(セーフコール)、?:(エルヴィス)、!!(アサーション)、?.let(条件付き実行) - 連鎖したセーフコール
a?.b?.c?.dは、1行で複数の階層にわたるnull許容参照をたどる - エルビス戦略:デフォルト値 / 例外のスロー / 早期リターン / requireNotNull
- プラットフォーム型
T!は、Java 相互運用におけるグレーゾーンです。これらを null 許容型として扱うことを推奨します。 !!は最後の手段です。安全なエルビス方式や?.let方式を優先してください。
📝 練習問題
- 初心者 (⭐): null許容の
String?変数を定義し、?.、?:、およびスマートキャストを使用してその長さを取得します。 ヒント:str?.length、str?.length ?: 0、if (str != null) - 中級 (⭐⭐): 連鎖したセーフコールを使用して、
Orderから都市名を取得し、null の場合は「Unknown」を返す。ヒント:order?.customer?.address?.city ?: "Unknown" - 上級 (⭐⭐⭐):
requireNotNullと Elvis を組み合わせた検証システムを設計し、Order のすべてのnull許容フィールドを検証し、null値に対して意味のあるビジネス例外をスローするようにしてください。ヒント:requireNotNullとカスタム例外型を組み合わせてください。