제네릭을 사용하면 타입 파라미터(Type parameter)를 받는 타입을 정의할 수 있다. 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자(Type Argument)로 치환해야한다.

자바와 달리 코틀린에서는 제네릭 타입의 타입 인자를 프로그래머가 명시하거나 컴파일러가 추론할 수 있어야 한다. 자바는 제네릭 지원이 1.5에 도입되어 이전 버전과 호환성을 유지하기 위해 타입 인자가 없는 제네릭 타입(raw Type)을 허용한다. 예를 들어 자바에서는 리스트 원소 타입을 지정하지 않고 List 타입의 변수를 선언할 수도 있다. 코틀린은 처음부터 제네릭을 도입했기 때문에 raw type을 지원하지 않고 제네릭 타입의 타입 인자를 항상 정의해야 한다.

제네릭 함수 예시는 다음과 같다.

fun <T> List<T>.slice(indices: IntRange) : List<T>

함수의 타입 파라미터 T가 수신 객체(List<T>.)와 반환 타입(: List<T>)에 쓰인다.

제네릭 확장 프로퍼티를 선언할 수 있다.

fun main() {
    println(listOf(1,2,3,4).penultimate) // 3
}

val <T> List<T>.penultimate: T
    get() = this[size-2]

확장이 아닌 일반 프로퍼티는 타입 파라미터를 가질 수 없다. 클래스 프로퍼티에 여러 타입의 값을 저장할 수는 없기 때문이다.

class Some {
    val <T> x: T = TODO() // Type parameter of a property must be used in its receiver type
}

제네릭 클래스 선언

타입 파라미터를 넣은 꺾쇠 기호(<>)를 클래스 이름 뒤에 붙이면 클래스와 인터페이스를 제네릭하게 만들 수 있다. 타입 파라미터를 이름 뒤에 붙이고 나면 클래스 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있다.

표준 자바 인터페이스인 List를 간단하게 코틀린으로 정의하면 아래와 같다. 9장 뒤에서 변성(variance)에 대해 설명하면서 이 예제를 더 개선할 것이다.

interface List<T> {
    operator fun get(index: Int): T
    // ...
}

제네릭 클래스를 확장하는 클래스(또는 제네릭 인터페이스를 구현하는 클래스)를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다. 이때 구체적인 타입을 넘길 수도 있고 (하위 클래스도 제네릭 클래스라면) 타입 파라미터로 받은 타입을 넘길 수도 있다.

이전 List를 상속하는 클래스를 예시로 들어보자

class StringList: List<String> { // 구체적인 타입 인자 String을 지정한다.
    override fun get(index: Int): String {
        TODO("Not yet implemented")
    }
}
class ArrayList<T>: List<T> { // 제네릭 타입 파라미터 T를 List의 타입 인자로 넘긴다.
    override fun get(index: Int): T {
        TODO("Not yet implemented")
    }
}

하위 클래스에서 상위 클래스에 정의된 함수를 오버라이드하거나 사용하려면 타입 인자 T를 구체적 타입 String으로 치환해야 한다. ArrayList 클래스는 자신만의 타입 파라미터 T를 정의하면서 그 T를 기반 클래스의 타입 인자로 사용한다. ArrayList의 T는 List의 T와 전혀 다른 타입 파라피터이고, T가 아닌 다른 이름을 사용해도 의미에는 아무 차이가 없다.