Kotlin: Kotlinのnull安全性を徹底理解

最終更新:2026-08-26

Kotlinのnull安全性は、単なる?構文上の糖衣に留まらず、NullPointerExceptionをランタイムの爆弾からコンパイル時の制約へと変える、型システムの根本的な設計そのものです。Charlieはたった1行のコードで、3層にわたるnull許容参照を安全に処理しています。

1. 学習内容


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) オペレーター決定木

100%
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 ?: throwrequireNotNull の違いは何ですか?
A 機能的には同等です。 requireNotNullはより意味論的(パラメータの妥当性チェックの意図を表現)であるのに対し、?: throwはより柔軟性が高い(カスタム例外)。パラメータの妥当性チェックにはrequireNotNullを優先してください。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): null許容の String? 変数を定義し、?.?:、およびスマートキャストを使用してその長さを取得します。 ヒント:str?.lengthstr?.length ?: 0if (str != null)
  2. 中級 (⭐⭐): 連鎖したセーフコールを使用して、Order から都市名を取得し、null の場合は「Unknown」を返す。ヒント: order?.customer?.address?.city ?: "Unknown"
  3. 上級 (⭐⭐⭐): requireNotNull と Elvis を組み合わせた検証システムを設計し、Order のすべてのnull許容フィールドを検証し、null値に対して意味のあるビジネス例外をスローするようにしてください。ヒント:requireNotNull とカスタム例外型を組み合わせてください。

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%