Kotlin의 제네릭은 Java와 비슷하지만 inout 키워드를 통해 더 안전한 타입 시스템을 제공합니다.

기본 제네릭

클래스

class Box<T>(t: T) {
    var value = t
}

val box: Box<Int> = Box<Int>(1)

// 타입 추론 가능한 경우 생략
val box = Box(1)

함수

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts)
        result.add(t)
    return result
}

공변성과 반공변성

Food -> FastFood -> Burger 순으로 클래스가 확장된다고 가정합니다.

out 키워드 (공변성, Covariance)

Java의 ? extends T와 동일합니다.

  • 의미: T를 출력(return)만 하고, 입력(parameter)으로 받지 않음
  • 용어: Producer (생산자)
  • 가능한 할당: Source<String> -> Source<Any>
abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs  // OK! T는 out 파라미터
}

지역 변수에서 out 사용

서브타입을 파라미터로 입력할 수 있습니다.

fun copy(from: Array<out Any>, to: Array<Any?>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any?> = arrayOfNulls(3)
copy(ints, any)  // ints를 from에 전달 가능

in 키워드 (반공변성, Contravariance)

Java의 ? super T와 동일합니다.

  • 의미: T를 입력(parameter)으로만 받고, 출력하지 않음
  • 용어: Consumer (소비자)
  • 가능한 할당: Comparable<Number> -> Comparable<Double>
abstract class Comparable<in T> {
    abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0)  // Double은 Number의 하위 타입
    val y: Comparable<Double> = x  // OK!
}

지역 변수에서 in 사용

슈퍼타입을 파라미터로 입력할 수 있습니다.

fun fill(dest: Array<in Int>, value: Int) {
    dest[0] = value
}

val objects: Array<Any?> = arrayOfNulls(1)
fill(objects, 1)  // objects를 dest에 전달 가능
assertEquals(objects[0], 1)

Star Projection (*)

in이나 out이 없는 경우, *로 어떤 타입이든 올 수 있음을 나타냅니다.

fun printArray(array: Array<*>) {
    array.forEach { println(it) }
}

제네릭 제약

Upper Bound

Java의 extends와 동일합니다.

fun <T : Comparable<T>> sort(list: List<T>) { ... }

Multiple Upper Bounds

여러 제약을 걸 때는 where 절을 사용합니다.

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable<T>,
          T : Cloneable {
    return list.filter { it > threshold }.map { it.clone() }
}

제네릭 확장 함수

모든 클래스에 적용되는 확장 함수를 만들 수 있습니다.

fun <T> T.basicToString(): String {
    return ""
}

Platform Type (!)

Java에서 온 타입은 T!로 표현되며, T 또는 T?일 수 있습니다.

// T! means "T or T?"

Reified 타입

inline 함수에서 제네릭 타입 정보를 런타임에 사용할 수 있습니다.

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

// 사용
treeNode.findParentOfType<MyTreeNode>()
object YamlReader {
    inline fun <reified T> read(path: String): T =
        ObjectMapper(YAMLFactory()).readValue(File(path), T::class.java)
}

정리: in과 out 사용 시점

상황 키워드 예시
값을 생산(return)만 함 out Source<out T>, List<out T>
값을 소비(parameter)만 함 in Comparable<in T>, Consumer<in T>
값을 생산하고 소비함 없음 MutableList<T>

다음 단계

제네릭에 대해 알아보았습니다. 다음으로 확장 함수에 대해 알아보세요.