이번 장에서는 제네릭에 대한 자세한 설명과 함께, 코틀린에서 새롭게 도입된 컨셉을 살펴볼 것이다. reified type parameter
와 declaration-site variance
등의 새로운 개념에 대해서 학습해보고자 한다.
제네릭은 타입 파라미터를 정의내릴 수 있도록 도와준다. 인스턴스가 생성되는 순간, 타입 파라미터는 타입 인수로 변경된다. 예를 들어, Map<K, V>
라고 선언한 후 Map<String, Person>
와 같이 특정한 인자로 변경할 수 있다.
//string을 인자로 넘겨주기 때문에 자동으로 List<String>
val authors = listOf("Dmitry", "Svetlana")
//빈 리스트를 생성하기 때문에 List<String>라는 것을 명시적으로 표시
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
<aside> 📌 코틀린은 반드시 제네릭 타입의 타입 인자를 정의해야한다:
자바의 경우 최초에는 제네릭이라는 개념이 없었고, 이후 업데이트를 통해 1.5에서 처음 제네릭이 나왔다. 이로 인해 자바에서는 컬렉션을 선언할 때 원소 타입을 지정하지 않아도 컬렉션을 생성할 수 있었다. 그러나 코틀린은 처음부터 제네릭을 도입했기 때문에 반드시 제네릭 타입의 인자를 정의해주어야(프로그래머가 정의하든, 타입 추론에 의해 정의되든) 사용할 수 있다.
</aside>
리스트를 사용하는 함수이지만 그 어떤 타입의 리스트인 간에 상관없이 사용할 수 있게 만들고 싶다면 제너릭 함수
를 만들어야 한다. 제너릭 함수는 자신만의 타입 파라미터를 가지고 있다. 이러한 타입 파라미터는 함수가 깨어날 때 특정한 인자로 변경되어야 한다.
위의 slice 함수를 호출할 때는 타입 인자를 명시적으로 표현해야 한다. 그러나 많은 경우 컴파일러가 이를 추론하기 때문에 그럴 필요가 없다. 예를 들어, letters.slice(10..13)
와 같이 호출한다면 컴파일러는 타입 파라미터 T가 Char라는 것을 자동으로 추론한다. 즉, letters.slice<Char>(0..2)
처럼 직접 명시해 줄 필요가 없다.
val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>(/* ... */)
fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>
>>> readers.filter { it !in authors }
위의 예시에서, it
은 String
이 된다. 컴파일러는 이를 추론하여 제너릭 타입 T
가 String
이 된다는 것을 알아낼 수 있다.
<aside> 📌 non-extension property에 대해 제너릭 선언 불가
</aside>
코틀린은 자바와 마찬가지로 꺽쇠 기호<>
를 사용하여 클래스나 인터페이스를 제네릭하게 만들 수 있다. 이렇게 정의내린 후에는 타입 파라미터를 클래스의 본문에서 사용할 수 있다. 예시로 코틀린의 List 인터페이스를 살펴보자.
interface List<T> { //타입 파라미터 T를 정의내림
operator fun get(index: Int): T //T는 레귤러 타입과 마찬가지로 인터페이스나 클래스에서 사용 가능
// ...
}
기본적으로, 코틀린의 제네릭은 자바와 매우 비슷하다. 이제부터는 다른 점을 살펴보도록 하자.
type paramter constraint
는 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다. 예를 들어, 리스트에 있는 원소를 모두 더하는 함수가 있다고 가정하자. 이 함수는 List<Int>나 List<Double>에 사용될 수 있지만, List<String>에는 사용 불가능하다. 이를 표현하기 위해서 타입 파라미터는 숫자만 될 수 있다는 것을 명시해야 한다.
어떤 타입을 제네릭 타입의 타입 파라미터에 대한 **상한(upper bound)**으로 지정하면 그 제네릭 타입을 인스턴화할 때 사용하는 타입 인자는 반드시 그 상한 타입이거나 그 상한 타입의 하위 타입이어야 한다.