Generic

Generic

클래스와 인터페이스의 매개변수 또는 함수의 매개변수와 반환 타입을 미리 확정하지 않고 정의한 후에 사용되는 시점에서 특정 타입을 지정할 수 있도록 해주는 기법

제네릭 클래스의 인스턴스 타입

원시 타입과 제네릭 타입이 결합된 것이 자신의 타입이 된다.

// List<Int> 타입
val listOfInts: List<Int> = listOf(1, 2, 3)

// List<Room> 타입
val listOfRooms: List<Room> = listOf(Room("room1"), Room("room2"))

제네릭 타입 매개변수의 표준 명칭

제네릭 타입 정의 및 사용

Generic 클래스 생성하기

class LootBox<T>(item: T) {
    private var loot: T = item
}

class Fedora(val name: String, val value: Int)

class Coin(val value: Int)

fun main(args: Array<String>) {
    val lootBoxOne: LootBox<Fedora> = LootBox(Fedora("평범한 중절모", 15))
    val lootBoxTwo: LootBox<Coin> = LootBox(Coin(15))
}

Generic을 사용해서 서로 다른 타입의 인스턴스를 갖는 LootBox 인스턴스를 생성한다.

제네릭 함수

class LootBox<T>(item: T) {
    var open = false
    private var loot: T = item
    
    fun fetch(): T? {
        return loot.takeIf { open }
    }
}

fun main(args: Array<String>) {
    val lootBoxOne: LootBox<Fedora> = LootBox(Fedora("평범한 중절모", 15))
    val lootBoxTwo: LootBox<Coin> = LootBox(Coin(15))

    lootBoxOne.open = true
    lootBoxOne.fetch()?.run {
        println("$name 를 LootBox에서 꺼냈습니다.")
    }   
}

복합 제네릭 타입 매개변수

제네릭 함수나 타입의 매개변수에는 또 다른 제네릭 타입 매개변수를 사용할 수도 있다.

class LootBox<T>(item: T) {
    var open = false
    private var loot: T = item

    fun fetch(): T? {
        return loot.takeIf { open }
    }

    fun <R> fetch(lootModFunction: (T) -> R): R? {
        return lootModFunction(loot).takeIf { open }
    }
}

fun main(args: Array<String>) {
    val lootBoxOne: LootBox<Fedora> = LootBox(Fedora("평범한 중절모", 15))
    val lootBoxTwo: LootBox<Coin> = LootBox(Coin(15))

    lootBoxOne.open = true
    lootBoxOne.fetch()?.run {
        println("$name 를 LootBox에서 꺼냈습니다.")
    }

    val coin = lootBoxOne.fetch {
        Coin(it.value * 3)
    }
    coin?.let { println(it.value) }
}

제네릭 타입 제약

Loot의 서브클래스만 LootBox 클래스의 매개변수 타입으로 사용할 수 있다.

class LootBox<T: Loot>(item: T) {
    var open = false
    private var loot: T = item

    fun fetch(): T? {
        return loot.takeIf { open }
    }

    fun <R> fetch(lootModFunction: (T) -> R): R? {
        return lootModFunction(loot).takeIf { open }
    }
}

open class Loot(val value: Int)

class Fedora(val name: String, value: Int): Loot(value)

class Coin(value: Int): Loot(value)

in, out

class Barrel<T>(var item: T)

fun main(args: Array<String>) {
    var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("평범한 중절모", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))

    // 에러 발생
    lootBarrel = fedoraBarrel
}

제네릭 클래스의 인스턴스는 원시 타입과 제네릭 타입이 결합된 것이다.
즉 fedoraBarrel의 타입은 Barrel이 아닌 Barrel이며 lootBarrel의 타입은 Barrel이다. <>로 나타낸 제네릭 타입 간의 슈퍼-서브 관계가 있더라도 컴파일러가 인식하지 못한다. 따라서 lootBarrel = fedoraBarrel 이 코드는 에러가 발생한다.

이런 문제를 해결하는 방법이 in, out 키워드이다.

out 키워드 추가하기

class Barrel<out T>(val item: T)

기존 소스와 다른점

reified

컴파일된 JVM 바이트코드에는 제네릭 타입 매개변수의 정보가 소거되기 때문에 타입 검사를 할 수 없다. reified 키워드와 inline 키워드를 함께 지정하면 타입 매개변수의 타입 검사를 런타임 시에 할 수 있다.

Reference