※本サイトで紹介している商品・サービス等の外部リンクには、アフィリエイト広告が含まれる場合があります。

11-1. ジェネリック制約

概要

Kotlin のジェネリクスは、型パラメータを使用することで、さまざまな型に対応した汎用的なクラスや関数を定義できる強力な機能です。
しかし、すべての型がすべての状況に適しているわけではなく、特定の条件を満たす型だけを許可したい場合があります。
そのために、Kotlin ではジェネリック制約(Generic Constraints)という機能が用意されています。

上限境界 (Upper Bounds)

Kotlin では、ジェネリック型パラメータに対して上限境界を指定できます。これにより、ある特定の型、またはそのサブクラスに限定した処理が可能になります。
T : SomeClass のように記述し、型パラメータ T が SomeClass またはそのサブタイプであることを指定します。

例:
以下の例では、T が Number クラスを上限境界として指定されています。そのため、Number のサブクラスである Int や Double は使用できますが、String のような Number とは無関係な型は許可されません。
関数 sum は、T 型の 2 つの引数を受け取りますが、T の型は Number クラスを上限境界とするため、Int や Double のような数値型が渡されます。
それぞれの数値は toDouble() メソッドで Double に変換され、加算処理が行われます。

fun <T : Number> sum(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}

fun main() {
    val result1 = sum(10, 20)      // Int 型
    val result2 = sum(1.5, 2.5)    // Double 型
    
    println("Result1: $result1")   // 30.0
    println("Result2: $result2")   // 4.0

    // コンパイルエラー: String は Number のサブクラスではない
    // val error = sum("10", "20")
}

複数の制約 (Multiple Constraints)

Kotlin では、複数の制約を同時に指定することができます。これは、ある型が複数のインターフェースやクラスを実装している必要がある場合に有効です。Kotlin では、最初の制約にクラス、以降の制約にはインターフェースを指定することが可能です。

例:
以下の例では、型パラメータ T に Comparable と CharSequence の 2 つの制約を課しています。T は Comparable でなければならず、同時に CharSequence を実装している必要があります。
関数 compareAndPrint は、型 T が Comparable であり、かつ CharSequence を実装している場合に使用できます。String はこの両方を実装しているため、問題なく動作します。
Int のような型は Comparable を実装していますが、CharSequence ではないため、コンパイル時にエラーとなります。

fun <T> compareAndPrint(item1: T, item2: T) where T : Comparable<T>, T : CharSequence {
    if (item1 > item2) {
        println("$item1 is greater than $item2")
    } else if (item1 < item2) {
        println("$item1 is less than $item2")
    } else {
        println("$item1 is equal to $item2")
    }
}

fun main() {
    compareAndPrint("apple", "banana")  // String は CharSequence かつ Comparable
    // コンパイルエラー: Int は CharSequence を実装していない
    // compareAndPrint(1, 2)
}

型パラメータの Null 非許容 (Non-nullable Constraints)

Kotlin では、ジェネリック型パラメータに対して「null 非許容」の制約を指定することも可能です。これにより、ジェネリック型が null を許可しない型として扱われ、null に関連するエラーを防ぐことができます。

例 :
以下の例では、T : Any として型パラメータ T を指定し、null を許可しない制約を追加しています。
T : Any とすることで、型 T が非 null 型であることを保証します。これにより、null を渡そうとするとコンパイルエラーが発生します。

fun <T : Any> printNonNull(item: T) {
    println(item)
}

fun main() {
    printNonNull("Kotlin")  // 問題なく動作
    // コンパイルエラー: null は Any のサブタイプではない
    // printNonNull(null)
}

使用例

ジェネリック制約は、ライブラリ設計やコードの再利用において特に有効です。以下は、maxOf という 2 つの値のうち大きい方を返す汎用的な関数をジェネリック制約を使って実装した例です。
関数 maxOf は、T が Comparable を実装していることを要求します。これにより、T 型の 2 つの引数 a と b を比較して、大きい方を返すことができます。
Int や String は Comparable を実装しているため、この関数を利用できます。

fun <T : Comparable<T>> maxOf(a: T, b: T): T {
    return if (a > b) a else b
}

fun main() {
    println(maxOf(10, 20))       // Int 型
    println(maxOf("apple", "banana"))  // String 型
}

まとめ

Kotlin のジェネリック制約は、型安全性を確保しつつ、コードの再利用性を高めるための強力なツールです。上限境界、複数制約、そして null 非許容制約を使いこなすことで、より柔軟で安全なコードを記述することが可能です