Kotlin: Kotlinの拡張関数

最終更新:2026-08-26

拡張関数を使えば、Charlieはソースコードを変更することなく、isHighValue()Order に追加することができます。これは魔法ではなく、コンパイル時の静的ディスパッチを簡略化した構文上の糖衣に過ぎませんが、その実用的な価値は否定できません。

1. 学習内容


2. 本物の建築家の物語

(1) 課題:ユーティリティクラスの乱立

CharlieのJavaプロジェクトには、15個のUtilクラスが存在します:OrderUtilStringUtilDateUtil……。それぞれが静的メソッドの寄せ集めとなっており、OrderUtil.isHighValue(order)のような呼び出しは冗長で直感的ではありません。

(2) 拡張関数による解法

KOTLIN
// Instead of OrderUtil.isHighValue(order)
fun Order.isHighValue() = total > 10_000

// Now call it like a member method!
if (order.isHighValue()) {
    routeToVipPipeline()
}

拡張関数は、API呼び出しをUtil.method(obj)からobj.method()に変換し、コードの可読性を飛躍的に向上させます。


3. 拡張関数の基礎

(1) 基本的な構文

KOTLIN
// Extend String with order ID formatting
fun String.toOrderId() = "ORD-$this"

// Extend Double with USD formatting
fun Double.toUSD() = "\$$this USD"

// Extend List with business logic
fun List<Order>.totalRevenue() = this.sumOf { it.total }

// Usage
println("001".toOrderId())          // ORD-001
println(299.99.toUSD())             // $299.99 USD
println(orders.totalRevenue())      // 12345.67

(2) ジェネリクスを使用した拡張関数

KOTLIN
// Generic extension
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// Extension with type constraint
fun <T : Comparable<T>> List<T>.secondLargest(): T? {
    return this.sortedDescending().secondOrNull()
}

(3) null許容レシーバー拡張

KOTLIN
// Extend nullable type - handle null gracefully
fun String?.orDefault(default: String = "N/A"): String = this ?: default

val name: String? = null
println(name.orDefault("Unknown"))  // Unknown
println("Alice".orDefault())        // Alice

4. 拡張プロパティ

KOTLIN
// Read-only extension property
val BigDecimal.inMillions: Double
    get() = this.toDouble() / 1_000_000

val String.isOrderId: Boolean
    get() = startsWith("ORD-")

// Usage
val revenue = BigDecimal("2_500_000")
println("${revenue.inMillions}M")  // 2.5M

println("ORD-001".isOrderId)  // true
println("ABC-001".isOrderId)  // false

// Note: extension properties CANNOT have backing fields
// var extension properties are possible but require explicit setter

(1) 拡張プロパティとメンバープロパティ

次元 メンバープロパティ 拡張プロパティ
定義場所 クラス内部 クラス外部
バッキング・フィールド あり なし
状態の保存 オブジェクト内に保存 保存不可
可変性 var として使用可能 計算変数のみ
アクセスレベル 可視性の対象 公開APIのみにアクセス

5. 静的ディスパッチ — 拡張関数の中核

拡張関数は静的にディスパッチされます。つまり、どの実装が呼び出されるかは、実行時の型ではなく、宣言された型によって決定されます。

(1) 静的ディスパッチの例

KOTLIN
open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)

// Extension on Order
fun Order.summary() = "Order $id: \$$total USD"

// Extension on BulkOrder
fun BulkOrder.summary() = "Bulk $id: \$$total USD (min: $minQty)"

fun printSummary(order: Order) {
    println(order.summary())  // Always calls Order.summary()!
}

val bulk = BulkOrder("ORD-001", 5_000.0, 100)
printSummary(bulk)       // "Order ORD-001: $5000.0 USD" - NOT BulkOrder version!
bulk.summary()           // "Bulk ORD-001: $5000.0 USD (min: 100)" - direct call OK

(2) 静的ディスパッチのシーケンス図

100%
sequenceDiagram
    participant Caller
    participant Order
    participant BulkOrder

    Caller->>Order: order.summary() (declared type: Order)
    Note over Order: Resolved at COMPILE TIME
    Order-->>Caller: Order.summary() result

    Caller->>BulkOrder: bulk.summary() (declared type: BulkOrder)
    Note over BulkOrder: Resolved at COMPILE TIME
    BulkOrder-->>Caller: BulkOrder.summary() result

(3) 静的ディスパッチと仮想メソッドのディスパッチ

次元 メンバー関数(仮想ディスパッチ) 拡張関数(静的ディスパッチ)
解決時間 実行時間 コンパイル時間
基数 実際の型 宣言された型
多態性 対応 非対応
オーバーライド サブクラスでオーバーライド可能 オーバーライド不可
メリット ダイナミックな柔軟性 安全で予測可能

6. スコープ管理

(1) トップレベル拡張子

KOTLIN
// File: OrderExtensions.kt
package com.order.extensions

fun Order.isHighValue() = total > 10_000
fun List<Order>.totalRevenue() = sumOf { it.total }

(2) クラスメンバーの拡張

KOTLIN
class OrderService {
    // Extension defined inside a class - only visible within this class
    fun Order.needsReview(): Boolean = total > 5_000 && status == "PENDING"

    fun process(order: Order) {
        if (order.needsReview()) {  // Accessible here
            routeToReview(order)
        }
    }
}

// order.needsReview()  // ERROR: not accessible outside OrderService

(3) 拡張範囲の比較

定義・場所 可視性 ユースケース
最上位(ファイルレベル) プロジェクト全体(インポート後) 汎用ユーティリティ拡張関数
クラスメンバー内部 クラス内部のみ クラスの状態に紐づく拡張関数
同一ファイル 同一ファイルのみ 補助拡張関数

7. 完全な例:OrderProcessor 拡張ツールキット

▶ サンプル:OrderProcessor拡張ツールキット

KOTLIN
// ============================================
// OrderProcessor - Extension Toolkit
// Feature: Business logic as extension functions
// ============================================

import java.math.BigDecimal
import java.math.RoundingMode

data class Order(val id: String, val total: Double, val status: String, val customer: String)

// String extensions
fun String.toOrderId() = if (startsWith("ORD-")) this else "ORD-$this"
fun String.isOrderId() = matches(Regex("ORD-\\d{3,}"))

// Double extensions
fun Double.toUSD(): String = "\$${"%.2f".format(this)} USD"
fun Double.inMillions(): Double = this / 1_000_000

// BigDecimal extensions
fun BigDecimal.toUSD(): String = "\$${setScale(2, RoundingMode.HALF_UP)} USD"

// Order extensions
fun Order.isHighValue() = total > 10_000
fun Order.isPending() = status == "PENDING"
fun Order.summary() = "$id | ${total.toUSD()} | $status | $customer"

// List<Order> extensions
fun List<Order>.totalRevenue() = sumOf { it.total }
fun List<Order>.highValueOrders() = filter { it.isHighValue() }
fun List<Order>.byCustomer() = groupBy { it.customer }
fun List<Order>.revenueByCustomer() = byCustomer().mapValues { (_, orders) -> orders.totalRevenue() }

// Nullable extension
fun String?.orDefault(default: String = "UNKNOWN") = this ?: default

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99, "CONFIRMED", "Alice"),
        Order("ORD-002", 15_000.00, "PENDING", "Bob"),
        Order("ORD-003", 2_500.00, "SHIPPED", "Charlie"),
        Order("ORD-004", 8_900.00, "CONFIRMED", "Bob"),
        Order("ORD-005", 45.50, "CANCELLED", "Alice")
    )

    // String extension
    println("001".toOrderId())       // ORD-001
    println("ORD-001".toOrderId())   // ORD-001
    println("ORD-001".isOrderId())   // true

    // Order extensions
    println("\n=== Order Summaries ===")
    orders.forEach { println(it.summary()) }

    println("\n=== High Value Orders ===")
    orders.highValueOrders().forEach { println(it.summary()) }

    // List extensions
    println("\n=== Revenue ===")
    println("Total: ${orders.totalRevenue().toUSD()}")
    println("In millions: ${orders.totalRevenue().inMillions()}M")

    println("\n=== Revenue by Customer ===")
    orders.revenueByCustomer().forEach { (customer, revenue) ->
        println("  $customer: ${revenue.toUSD()}")
    }

    // Nullable extension
    val name: String? = null
    println("\nDefault name: ${name.orDefault("Guest")}")
}

出力:

TEXT 📖 参照専用
ORD-001
ORD-001
true

=== Order Summaries ===
ORD-001 | $299.99 USD | CONFIRMED | Alice
ORD-002 | $15000.00 USD | PENDING | Bob
ORD-003 | $2500.00 USD | SHIPPED | Charlie
ORD-004 | $8900.00 USD | CONFIRMED | Bob
ORD-005 | $45.50 USD | CANCELLED | Alice

=== High Value Orders ===
ORD-002 | $15000.00 USD | PENDING | Bob

=== Revenue ===
Total: $26745.49 USD
In millions: 0.02674549M

=== Revenue by Customer ===
  Alice: $345.49 USD
  Bob: $23900.00 USD
  Charlie: $2500.00 USD

Default name: Guest

❓ よくある質問

Q 拡張関数はプライベートメンバーにアクセスできますか?
A いいえ。拡張関数はクラスの外部で定義されるため、パブリックメンバーにしかアクセスできません。これが、拡張関数がカプセル化を破らないための重要なポイントです。
Q 拡張関数の名前がメンバメソッドと同じ場合はどうなりますか?
A メンバメソッドが優先されます。クラスにすでに同じ名前とシグネチャを持つメソッドが存在する場合、拡張関数は無視されます。これが、拡張関数が安全である理由です。つまり、既存の挙動を誤って上書きしてしまうことは決してありません。
Q 拡張関数は継承やオーバーライドが可能ですか?
A いいえ。拡張関数は静的にディスパッチされ、仮想メソッドのディスパッチには関与しません。サブクラスは親クラスの拡張関数をオーバーライドすることはできません。
Q なぜ拡張プロパティにはバッキングフィールドを設定できないのですか?
A 拡張プロパティはオブジェクトとして保存されるものではなく、単なる計算ロジックだからです。状態を保存する必要がある場合は、他の仕組み(例:Mapアソシエーション)を使用する必要があります。
Q トップレベルの拡張関数は名前空間を乱しますか?
A 使用するにはインポートが必要であるため、自動的に名前空間を乱すことはありません。関連する拡張関数を同じファイルにまとめ、必要に応じてインポートすることをお勧めします。
Q 拡張関数のパフォーマンスは、通常のメソッドと同じですか?
A ほぼ同じです。拡張関数は静的メソッド呼び出しにコンパイルされ、JVM によって容易にインライン化および最適化されます。追加のオーバーヘッドは一切ありません。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): String に、指定された正規表現パターンが含まれているかどうかを確認する拡張関数 containsPattern を追加してください。ヒント:Regex.containsMatchIn を使用してください。
  2. 中級 (⭐⭐): StringtoOrderId() および isOrderId() の拡張関数を追加してください。ヒント:「ORD-」を先頭に付加する/正規表現で検証する
  3. 上級 (⭐⭐⭐): 拡張関数の静的ディスパッチを確認する:OrderBulkOrder を定義し、それぞれに同名の拡張関数を割り当て、Order 型の参照を通じて呼び出し、その結果を確認する。 ヒント:宣言された型によって呼び出しが決定されます

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%