Kotlin: Kotlinの拡張関数
最終更新:2026-08-26
拡張関数を使えば、Charlieはソースコードを変更することなく、isHighValue() を Order に追加することができます。これは魔法ではなく、コンパイル時の静的ディスパッチを簡略化した構文上の糖衣に過ぎませんが、その実用的な価値は否定できません。
1. 学習内容
- 拡張関数:
fun ReceiverType.extensionName() - 拡張プロパティ:バッキングフィールドを持たない計算プロパティ
- 静的ディスパッチ:拡張関数はカプセル化を破らず、多態性にも関与しない
- スコープ制御:トップレベルとクラスメンバーの拡張
- チャリーの活躍:
Order.isHighValue()/List<Order>.totalRevenue()
2. 本物の建築家の物語
(1) 課題:ユーティリティクラスの乱立
CharlieのJavaプロジェクトには、15個のUtilクラスが存在します:OrderUtil、StringUtil、DateUtil……。それぞれが静的メソッドの寄せ集めとなっており、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) 静的ディスパッチのシーケンス図
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 によって容易にインライン化および最適化されます。追加のオーバーヘッドは一切ありません。
📖 まとめ
- 拡張関数
fun Type.name():ソースコードを変更せずにメソッドを追加する - 拡張プロパティ
val Type.name:計算プロパティを追加する。バッキングフィールドは許可されない - 拡張関数は静的にディスパッチされます。宣言された型によって呼び出しが決定され、ポリモーフィズムは発生しません。
- メンバーメソッドは、同名の拡張メソッドよりも優先されます。拡張メソッドによって既存の動作が損なわれることはありません。
- トップレベルの拡張関数にはインポートが必要です。クラスメンバーの拡張関数は、そのクラス内部でのみ利用可能です。
- 拡張関数により、APIが
Util.method(obj)からobj.method()へと進化する
📝 練習問題
- 初心者 (⭐):
Stringに、指定された正規表現パターンが含まれているかどうかを確認する拡張関数containsPatternを追加してください。ヒント:Regex.containsMatchInを使用してください。 - 中級 (⭐⭐):
StringにtoOrderId()およびisOrderId()の拡張関数を追加してください。ヒント:「ORD-」を先頭に付加する/正規表現で検証する - 上級 (⭐⭐⭐): 拡張関数の静的ディスパッチを確認する:
OrderとBulkOrderを定義し、それぞれに同名の拡張関数を割り当て、Order型の参照を通じて呼び出し、その結果を確認する。 ヒント:宣言された型によって呼び出しが決定されます