Kotlinのジェネリクスで柔軟なインターフェース設計を実現する方法

Kotlinのジェネリクスを使って柔軟なインターフェースを設計することは、コードの再利用性と保守性を向上させる重要なテクニックです。ジェネリクスを活用することで、異なる型に対して同じ処理を適用でき、型安全性も確保されます。本記事では、Kotlinにおけるジェネリクスの基本概念から、柔軟なインターフェース設計方法、実際の応用例やエラー対処法までを詳しく解説します。ジェネリクスを駆使することで、効率的で拡張性の高いKotlinプログラムを作成するための知識を習得しましょう。

目次
  1. Kotlinにおけるジェネリクスの基本概念
    1. ジェネリクスの基本的な構文
    2. 型パラメータの役割
    3. ジェネリクスを使うメリット
  2. ジェネリクスを使ったインターフェースの定義
    1. 基本的なジェネリクスインターフェースの定義
    2. ジェネリクスインターフェースの実装例
    3. 複数の型パラメータを持つインターフェース
    4. ジェネリクスインターフェースを使う利点
  3. 型パラメータの制約とその活用法
    1. 型パラメータの制約とは
    2. 基本的な型制約の例
    3. 複数の制約を持つ型パラメータ
    4. 型制約の活用法
  4. 共変性と反変性の概念
    1. 共変性(Covariance)
    2. 反変性(Contravariance)
    3. 共変性と反変性の使い分け
    4. 不変性(Invariance)
    5. まとめ
  5. インターフェースにおける型安全性の向上
    1. 型安全性とは
    2. ジェネリクスによるインターフェースの型安全性向上
    3. Nullable型の活用
    4. 境界付き型パラメータで型安全性を高める
    5. 型安全性を向上させるポイント
    6. まとめ
  6. 実用例:ジェネリクスを使ったリポジトリパターン
    1. リポジトリパターンとは
    2. ジェネリクスを用いたリポジトリインターフェースの定義
    3. 具象リポジトリの実装例
    4. リポジトリの柔軟な再利用
    5. リポジトリパターンの利点
  7. よくあるエラーとトラブルシューティング
    1. 1. 型推論エラー
    2. 2. 型パラメータの制約違反
    3. 3. 不変性による代入エラー
    4. 4. 型消去(Type Erasure)
    5. 5. Null安全性の問題
    6. まとめ
  8. 演習問題と解説
    1. 演習問題 1: ジェネリクス関数の作成
    2. 演習問題 2: 型制約を使った関数
    3. 演習問題 3: ジェネリクスを使ったインターフェース
    4. 演習問題 4: 共変性と反変性
    5. まとめ
  9. まとめ

Kotlinにおけるジェネリクスの基本概念


Kotlinのジェネリクスは、異なる型に対して共通の処理を適用できる仕組みです。これにより、コードの再利用性や型安全性が向上します。

ジェネリクスの基本的な構文


Kotlinでは、型パラメータを<>で指定することでジェネリクスを利用できます。例えば、List<T>のように定義します。

fun <T> printItem(item: T) {
    println(item)
}

printItem(5)        // 整数型
printItem("Hello")  // 文字列型

型パラメータの役割

  • T: 任意の型を示すために使われる型パラメータです。慣例として、シンプルなアルファベットが使われます。
  • 複数の型パラメータ: 複数の型パラメータを定義する場合は、カンマで区切ります。
  fun <K, V> pairToString(key: K, value: V): String {
      return "$key -> $value"
  }

ジェネリクスを使うメリット

  1. 再利用性の向上: 型に依存しない汎用的な関数やクラスが作れます。
  2. 型安全性の確保: コンパイル時に型エラーを防げます。
  3. コードの簡潔化: 重複するコードを減らし、シンプルに保てます。

Kotlinのジェネリクスは、柔軟なプログラミング設計の基盤となります。次は、ジェネリクスを使ったインターフェースの定義について見ていきましょう。

ジェネリクスを使ったインターフェースの定義


Kotlinでは、インターフェースにもジェネリクスを導入することで、異なる型に柔軟に対応できる設計が可能です。これにより、さまざまなデータ型に対して一貫した処理を提供できます。

基本的なジェネリクスインターフェースの定義


インターフェースでジェネリクスを使用する場合、型パラメータを<>内で指定します。以下は基本的な構文です。

interface Container<T> {
    fun add(item: T)
    fun get(): T
}

このContainer<T>インターフェースは、任意の型Tに対応する汎用的なコンテナを定義しています。

ジェネリクスインターフェースの実装例


具体的にインターフェースを実装する例を見てみましょう。

class StringContainer : Container<String> {
    private val items = mutableListOf<String>()

    override fun add(item: String) {
        items.add(item)
    }

    override fun get(): String {
        return items.last()
    }
}

fun main() {
    val container = StringContainer()
    container.add("Hello")
    container.add("Kotlin")
    println(container.get()) // 出力: Kotlin
}

複数の型パラメータを持つインターフェース


インターフェースは複数の型パラメータをサポートします。

interface PairContainer<K, V> {
    fun addPair(key: K, value: V)
    fun getPair(): Pair<K, V>
}

class IntStringPair : PairContainer<Int, String> {
    private var key: Int = 0
    private var value: String = ""

    override fun addPair(key: Int, value: String) {
        this.key = key
        this.value = value
    }

    override fun getPair(): Pair<Int, String> {
        return Pair(key, value)
    }
}

ジェネリクスインターフェースを使う利点

  1. 柔軟性: 様々な型に対して同じインターフェースを利用可能。
  2. コードの一貫性: 型ごとに異なるインターフェースを作る必要がなく、一貫した設計ができる。
  3. 型安全性: 実行時の型エラーを防ぎ、コンパイル時に問題を検出。

次は、型パラメータに制約を設ける方法とその活用法について解説します。

型パラメータの制約とその活用法


Kotlinでは、ジェネリクスの型パラメータに制約を付けることで、型の安全性をさらに向上させることができます。これにより、特定の型や型の振る舞いに限定したインターフェースやクラスを設計することが可能になります。

型パラメータの制約とは


型パラメータに制約を設けることで、指定する型が特定のクラスやインターフェースを継承していることを保証できます。Kotlinでは、<T : 型名>の形式で制約を指定します。

基本的な型制約の例


例えば、Number型を制約にしたジェネリクス関数を定義する場合:

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

fun main() {
    println(sumValues(5, 10))          // 出力: 15.0
    println(sumValues(3.5, 2.5))       // 出力: 6.0
    // println(sumValues("5", "10"))   // エラー: StringはNumberを継承していない
}

この場合、型パラメータTNumberクラスを継承する型に限定されます。

複数の制約を持つ型パラメータ


Kotlinでは、複数の制約を指定することも可能です。主な型制約はコロン:で指定し、複数のインターフェースを継承する場合はwhere句を使います。

interface Printable {
    fun print()
}

fun <T> display(item: T) where T : Number, T : Printable {
    item.print()
}

class PrintableInt(val value: Int) : Number(), Printable {
    override fun toDouble(): Double = value.toDouble()
    override fun toFloat(): Float = value.toFloat()
    override fun toInt(): Int = value
    override fun toLong(): Long = value.toLong()
    override fun print() = println("Value: $value")
}

fun main() {
    val item = PrintableInt(42)
    display(item)  // 出力: Value: 42
}

型制約の活用法

  1. 特定の型に依存した処理: 型パラメータを特定のクラスやインターフェースに制約することで、型に依存したメソッドを安全に呼び出せます。
  2. コードの安全性向上: 誤った型の使用を防ぎ、コンパイル時にエラーを検出できます。
  3. 汎用性と柔軟性の両立: 型制約を用いることで、柔軟なジェネリクス設計と安全性を両立できます。

次は、共変性と反変性の概念について解説します。

共変性と反変性の概念


Kotlinでは、ジェネリクスの型パラメータに「共変性」や「反変性」という性質を指定することで、柔軟かつ安全に型の継承関係を扱えます。これにより、インターフェースやクラスの型パラメータを適切に設計できます。

共変性(Covariance)


共変性は、型パラメータが上位型に代入可能であることを意味します。Kotlinではoutキーワードを使って共変性を示します。

構文例:

interface Producer<out T> {
    fun produce(): T
}

:

open class Animal  
class Dog : Animal()  

class AnimalProducer : Producer<Dog> {
    override fun produce(): Dog = Dog()
}

fun useProducer(producer: Producer<Animal>) {
    println(producer.produce())
}

fun main() {
    val dogProducer: Producer<Dog> = AnimalProducer()
    useProducer(dogProducer)  // 共変性によりProducer<Dog>はProducer<Animal>として扱える
}

ポイント:

  • outで宣言した型パラメータは、戻り値としてのみ使用できます(入力には使用不可)。
  • 共変性は「出力専用」と考えます。

反変性(Contravariance)


反変性は、型パラメータが下位型に代入可能であることを意味します。Kotlinではinキーワードを使って反変性を示します。

構文例:

interface Consumer<in T> {
    fun consume(item: T)
}

:

open class Animal  
class Dog : Animal()  

class AnimalConsumer : Consumer<Animal> {
    override fun consume(item: Animal) {
        println("Consuming animal")
    }
}

fun useConsumer(consumer: Consumer<Dog>) {
    consumer.consume(Dog())
}

fun main() {
    val animalConsumer: Consumer<Animal> = AnimalConsumer()
    useConsumer(animalConsumer)  // 反変性によりConsumer<Animal>はConsumer<Dog>として扱える
}

ポイント:

  • inで宣言した型パラメータは、引数としてのみ使用できます(戻り値には使用不可)。
  • 反変性は「入力専用」と考えます。

共変性と反変性の使い分け

  1. 共変性(out): 型パラメータが戻り値として使われる場合(データを提供する役割)。例: List<out T>
  2. 反変性(in): 型パラメータが引数として使われる場合(データを消費する役割)。例: Comparable<in T>

不変性(Invariance)


Kotlinのクラスやインターフェースは、デフォルトで不変です。つまり、型パラメータは指定された型そのものに限定され、上位型や下位型に代入できません。

:

class Box<T>(val item: T)
val stringBox: Box<String> = Box("Hello")
// val anyBox: Box<Any> = stringBox  // エラー: Box<String>はBox<Any>に代入できない

まとめ

  • 共変性(out): 上位型に代入可能。出力専用。
  • 反変性(in): 下位型に代入可能。入力専用。
  • 不変性: デフォルトの状態。型パラメータはその型に固定される。

次は、インターフェースにおける型安全性の向上について解説します。

インターフェースにおける型安全性の向上


Kotlinでは、ジェネリクスを活用することでインターフェースの型安全性を高めることができます。型安全性が向上すると、実行時エラーが減少し、コードの信頼性と保守性が向上します。

型安全性とは


型安全性とは、コンパイル時に型の整合性が保証されることを意味します。型安全性が確保されていると、不適切な型の操作によるエラーを未然に防ぐことができます。

型安全性が低い例(Javaの非ジェネリクス時代):

List list = new ArrayList();
list.add("Hello");
list.add(123);  // 意図しない型のデータが追加される
String str = (String) list.get(1);  // 実行時エラーが発生

Kotlinの型安全な例:

val list: MutableList<String> = mutableListOf()
list.add("Hello")
// list.add(123)  // コンパイル時にエラーが発生するため安全

ジェネリクスによるインターフェースの型安全性向上


ジェネリクスをインターフェースに導入することで、型安全性を保ちつつ柔軟な設計が可能になります。

型安全なインターフェース例:

interface Repository<T> {
    fun add(item: T)
    fun getAll(): List<T>
}

class StringRepository : Repository<String> {
    private val items = mutableListOf<String>()

    override fun add(item: String) {
        items.add(item)
    }

    override fun getAll(): List<String> = items
}

fun main() {
    val repo = StringRepository()
    repo.add("Kotlin")
    println(repo.getAll())  // 出力: [Kotlin]
    // repo.add(123)  // コンパイル時にエラーが発生する
}

Nullable型の活用


Kotlinでは、型安全性を保つためにNullable型とNon-Nullable型を区別します。

interface NullableContainer<T> {
    fun getItem(): T?
}

class SafeContainer : NullableContainer<String> {
    override fun getItem(): String? = null  // Nullを返すことが許容される
}

境界付き型パラメータで型安全性を高める


型パラメータに制約を設けることで、型安全性をさらに向上させることができます。

例: 数値型に限定したジェネリクス:

interface NumericProcessor<T : Number> {
    fun process(value: T): Double
}

class IntProcessor : NumericProcessor<Int> {
    override fun process(value: Int): Double = value.toDouble() * 2
}

fun main() {
    val processor = IntProcessor()
    println(processor.process(5))  // 出力: 10.0
}

型安全性を向上させるポイント

  1. ジェネリクスの使用: 型パラメータを活用し、特定の型に限定する。
  2. Nullable型の明示: Null許容型を明示し、NullPointerExceptionを防ぐ。
  3. 型制約の利用: 境界付き型パラメータを使って適切な型制約を設ける。

まとめ


ジェネリクスをインターフェースに導入することで、Kotlinでは型安全性を確保し、エラーを未然に防ぐ設計が可能です。次は、ジェネリクスを使ったリポジトリパターンの実用例について解説します。

実用例:ジェネリクスを使ったリポジトリパターン


ジェネリクスを活用すると、データ操作を抽象化したリポジトリパターンを柔軟に設計できます。これにより、異なるデータ型に対しても共通のデータアクセス処理を提供でき、コードの再利用性と保守性が向上します。

リポジトリパターンとは


リポジトリパターンは、データへのアクセス処理を抽象化し、ビジネスロジックとデータアクセスロジックを分離する設計パターンです。これにより、データソースが変わってもビジネスロジックを変更する必要がなくなります。

ジェネリクスを用いたリポジトリインターフェースの定義


型パラメータを用いたリポジトリのインターフェースを定義します。

interface Repository<T> {
    fun add(item: T)
    fun getAll(): List<T>
    fun findById(id: Int): T?
}

このインターフェースは、任意の型Tに対する基本的なデータ操作を提供します。

具象リポジトリの実装例


具体的なデータ型Userに対してリポジトリを実装してみましょう。

data class User(val id: Int, val name: String)

class UserRepository : Repository<User> {
    private val users = mutableListOf<User>()

    override fun add(item: User) {
        users.add(item)
    }

    override fun getAll(): List<User> = users

    override fun findById(id: Int): User? {
        return users.find { it.id == id }
    }
}

fun main() {
    val userRepository = UserRepository()
    userRepository.add(User(1, "Alice"))
    userRepository.add(User(2, "Bob"))

    println(userRepository.getAll())         // 出力: [User(id=1, name=Alice), User(id=2, name=Bob)]
    println(userRepository.findById(1))      // 出力: User(id=1, name=Alice)
    println(userRepository.findById(3))      // 出力: null
}

リポジトリの柔軟な再利用


リポジトリインターフェースを使えば、他のデータ型にも簡単に適用できます。

例: 商品データ用のリポジトリ:

data class Product(val id: Int, val name: String, val price: Double)

class ProductRepository : Repository<Product> {
    private val products = mutableListOf<Product>()

    override fun add(item: Product) {
        products.add(item)
    }

    override fun getAll(): List<Product> = products

    override fun findById(id: Int): Product? {
        return products.find { it.id == id }
    }
}

fun main() {
    val productRepository = ProductRepository()
    productRepository.add(Product(1, "Laptop", 999.99))
    productRepository.add(Product(2, "Phone", 499.99))

    println(productRepository.getAll())         // 出力: [Product(id=1, name=Laptop, price=999.99), Product(id=2, name=Phone, price=499.99)]
    println(productRepository.findById(2))      // 出力: Product(id=2, name=Phone, price=499.99)
}

リポジトリパターンの利点

  1. 再利用性: ジェネリクスにより、異なるデータ型に対応するリポジトリを簡単に作成できます。
  2. テストの容易さ: データアクセスが抽象化されているため、モックやスタブを使ったテストが容易です。
  3. 保守性の向上: データソースが変更されても、ビジネスロジックに影響を与えません。
  4. 一貫性: データ操作ロジックが統一され、コードの一貫性が保たれます。

次は、ジェネリクスを使う際によくあるエラーとそのトラブルシューティングについて解説します。

よくあるエラーとトラブルシューティング


Kotlinでジェネリクスを使用する際には、特有のエラーや問題が発生することがあります。これらのエラーを理解し、適切に対処することで、型安全で効率的なコードを書くことができます。

1. 型推論エラー


エラー例:

fun <T> printItem(item: T) {
    println(item)
}

val result = printItem(5) + 10  // エラー: printItemの戻り値がUnit型

原因: printItem関数の戻り値がUnit型であるため、数値と加算できません。
対処法: 戻り値が必要なら、関数に適切な戻り値を指定します。

fun <T> printItemAndReturn(item: T): T {
    println(item)
    return item
}

val result = printItemAndReturn(5) + 10  // 出力: 5, 正常に動作

2. 型パラメータの制約違反


エラー例:

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

sum("5", "10")  // エラー: StringはNumberのサブタイプではない

原因: 型パラメータTNumber型で制約されていますが、String型が渡されているためエラーになります。
対処法: 制約に合った型を渡します。

sum(5, 10)         // 正常に動作
sum(3.5, 2.5)     // 正常に動作

3. 不変性による代入エラー


エラー例:

val strings: List<String> = listOf("A", "B")
val anys: List<Any> = strings  // エラー: Listは不変

原因: Kotlinのコレクションは不変(invariant)であるため、List<String>List<Any>に代入できません。
対処法: 共変性(out)を利用することで解決できます。

val strings: List<String> = listOf("A", "B")
val anys: List<out Any> = strings  // 共変性により代入可能

4. 型消去(Type Erasure)


エラー例:

fun <T> checkType(list: List<T>) {
    if (list is List<String>) {  // エラー: ジェネリクスの型情報は実行時に消去される
        println("This is a list of strings")
    }
}

原因: ジェネリクスの型情報はコンパイル時に消去され、実行時には型がListとして扱われます。
対処法: 実行時の型チェックが必要な場合、reifiedキーワードを使用します。

inline fun <reified T> checkType(list: List<*>) {
    if (list is List<T>) {
        println("This is a list of ${T::class.simpleName}")
    }
}

fun main() {
    checkType<String>(listOf("A", "B"))  // 出力: This is a list of String
}

5. Null安全性の問題


エラー例:

fun <T> getFirstItem(list: List<T>): T {
    return list[0]  // エラー: listが空の場合、例外が発生する可能性
}

対処法: Nullable型や安全な呼び出し演算子を使用します。

fun <T> getFirstItem(list: List<T>): T? {
    return list.firstOrNull()
}

まとめ


Kotlinのジェネリクスを使用する際に起こりやすいエラーとその対処法について解説しました。型推論エラー、型パラメータの制約違反、不変性の問題、型消去、Null安全性の問題を理解し、適切に対処することで、型安全で柔軟なコードを作成できます。

次は、理解を深めるための演習問題とその解説を紹介します。

演習問題と解説


Kotlinのジェネリクスを使った柔軟なインターフェース設計を理解するために、いくつかの演習問題を用意しました。問題に取り組むことで、ジェネリクスの理解を深め、実践的なスキルを身につけましょう。


演習問題 1: ジェネリクス関数の作成


問題:
任意の型Tを受け取り、その型のリストに要素を追加し、リストを返す関数addToListを作成してください。

ヒント:

  • ジェネリクスの型パラメータを使用すること。
  • リストに要素を追加した後、そのリストを返す。

解答例:

fun <T> addToList(item: T, list: MutableList<T>): List<T> {
    list.add(item)
    return list
}

fun main() {
    val numbers = mutableListOf(1, 2, 3)
    println(addToList(4, numbers))  // 出力: [1, 2, 3, 4]

    val strings = mutableListOf("a", "b")
    println(addToList("c", strings))  // 出力: [a, b, c]
}

演習問題 2: 型制約を使った関数


問題:
Number型に制約されたジェネリクス関数multiplyByTwoを作成し、引数に渡された数値を2倍にして返してください。

ヒント:

  • 型パラメータTNumber型の制約を付けること。
  • toDouble()を使用して数値を2倍にする。

解答例:

fun <T : Number> multiplyByTwo(value: T): Double {
    return value.toDouble() * 2
}

fun main() {
    println(multiplyByTwo(5))        // 出力: 10.0
    println(multiplyByTwo(3.5))      // 出力: 7.0
}

演習問題 3: ジェネリクスを使ったインターフェース


問題:
ジェネリクスを使ったStorageインターフェースを作成し、任意の型Tを格納・取得する機能を持たせてください。その後、String型のデータを格納・取得するStringStorageクラスを実装してください。

ヒント:

  • Storageインターフェースにstoreメソッドとretrieveメソッドを定義する。

解答例:

interface Storage<T> {
    fun store(item: T)
    fun retrieve(): T
}

class StringStorage : Storage<String> {
    private var data: String = ""

    override fun store(item: String) {
        data = item
    }

    override fun retrieve(): String {
        return data
    }
}

fun main() {
    val storage = StringStorage()
    storage.store("Hello, Kotlin!")
    println(storage.retrieve())  // 出力: Hello, Kotlin!
}

演習問題 4: 共変性と反変性


問題:

  1. 共変性を持つProducer<out T>インターフェースを作成し、produceメソッドでデータを生成する機能を持たせてください。
  2. 反変性を持つConsumer<in T>インターフェースを作成し、consumeメソッドでデータを消費する機能を持たせてください。

解答例:

interface Producer<out T> {
    fun produce(): T
}

interface Consumer<in T> {
    fun consume(item: T)
}

class StringProducer : Producer<String> {
    override fun produce(): String = "Produced String"
}

class AnyConsumer : Consumer<Any> {
    override fun consume(item: Any) {
        println("Consumed: $item")
    }
}

fun main() {
    val producer: Producer<String> = StringProducer()
    val anyProducer: Producer<Any> = producer  // 共変性

    val consumer: Consumer<Any> = AnyConsumer()
    val stringConsumer: Consumer<String> = consumer  // 反変性

    stringConsumer.consume("Kotlin")
}

まとめ


これらの演習問題を通じて、Kotlinのジェネリクス、型制約、共変性、反変性についての理解が深まったはずです。次は、Kotlinのジェネリクスを使った柔軟なインターフェース設計についてのまとめを行います。

まとめ


本記事では、Kotlinのジェネリクスを使って柔軟なインターフェースを設計する方法について解説しました。ジェネリクスを活用することで、型安全性と再利用性を向上させ、異なる型に対して共通の処理を提供できる設計が可能です。

ジェネリクスの基本概念から始まり、型パラメータの制約、共変性と反変性、型安全性の向上、そしてリポジトリパターンを用いた実用例まで幅広く紹介しました。また、よくあるエラーとそのトラブルシューティング、理解を深めるための演習問題も提供しました。

Kotlinのジェネリクスをマスターすることで、柔軟で保守しやすいコードを書けるようになります。今後の開発で積極的に活用し、効率的なプログラム設計を目指しましょう。

コメント

コメントする

目次
  1. Kotlinにおけるジェネリクスの基本概念
    1. ジェネリクスの基本的な構文
    2. 型パラメータの役割
    3. ジェネリクスを使うメリット
  2. ジェネリクスを使ったインターフェースの定義
    1. 基本的なジェネリクスインターフェースの定義
    2. ジェネリクスインターフェースの実装例
    3. 複数の型パラメータを持つインターフェース
    4. ジェネリクスインターフェースを使う利点
  3. 型パラメータの制約とその活用法
    1. 型パラメータの制約とは
    2. 基本的な型制約の例
    3. 複数の制約を持つ型パラメータ
    4. 型制約の活用法
  4. 共変性と反変性の概念
    1. 共変性(Covariance)
    2. 反変性(Contravariance)
    3. 共変性と反変性の使い分け
    4. 不変性(Invariance)
    5. まとめ
  5. インターフェースにおける型安全性の向上
    1. 型安全性とは
    2. ジェネリクスによるインターフェースの型安全性向上
    3. Nullable型の活用
    4. 境界付き型パラメータで型安全性を高める
    5. 型安全性を向上させるポイント
    6. まとめ
  6. 実用例:ジェネリクスを使ったリポジトリパターン
    1. リポジトリパターンとは
    2. ジェネリクスを用いたリポジトリインターフェースの定義
    3. 具象リポジトリの実装例
    4. リポジトリの柔軟な再利用
    5. リポジトリパターンの利点
  7. よくあるエラーとトラブルシューティング
    1. 1. 型推論エラー
    2. 2. 型パラメータの制約違反
    3. 3. 不変性による代入エラー
    4. 4. 型消去(Type Erasure)
    5. 5. Null安全性の問題
    6. まとめ
  8. 演習問題と解説
    1. 演習問題 1: ジェネリクス関数の作成
    2. 演習問題 2: 型制約を使った関数
    3. 演習問題 3: ジェネリクスを使ったインターフェース
    4. 演習問題 4: 共変性と反変性
    5. まとめ
  9. まとめ