코틀린에서 제네릭은 기본적으로 자바와 동작이 비슷하다.
자바와 같이 <T>
와 타입을 한정할 수 있다.
상한 타입에 대해서는 extend를 쓰던 부분에서 코틀린의 상속, 구현을 의미하는 : 을 사용하면 된다.
<T : Number>
와 같이 T는 숫자 타입의 타입만으로 타입을 한정한다.
fun <T : Number> oneHalf(value: T): Double {
return value.toDouble() / 2.0
}
하나의 상한을 둘 경우는 위에 예제와 같이 사용하면 되지만 복수의 상한을 두고 싶다면 where 키워드를 이용해서 복수의 타입 상한을 둘 수 있다.
fun <타입파라미터> ex(param: T) where T : CharSequence, T : Appendable
<T>
는 ? 가 없기에 널이 될수 없는 타입이라 착각할 수 있다.
하지만 타입 파라미터 T는 널이 될 수 있는 타입도 포함한다.
따라서 모든 타입을 허용은 하지만 널이 될 수 없는 타입만 허용할려면
<T : Any>
와 같이 널이 될 수 없는 타입임을 명시적으로 작성해줘야한다.
제네릭은 타입 소거를 사용해 구현된다. 이 말은 실행 시점에 타입 인자 정보가 들어있지 않다는 뜻이다. 아래 코드를 보면서 타입 소거를 알아보자
fun main() {
test(listOf(1, 2, 3))
}
fun test(list: List<*>) {
val strings = list as? List<String>
if (strings != null) {
println("캐스팅 성공")
}
}
test() 메서드는 Int 타입의 리스트를 받았기에 당연히 as? String 타입 리스트로 캐스팅하면 null을 반환 할 줄 알았지만 타입소거로 인해 타입 파라미터는 Object 코틀린으로는 Any가 되기에 List<Any?>
로 타입 캐스팅이 되어 if문이 동작하게 된다.
즉 런타임 시점에는 List<Int> List<String>
은 같은 List가 된다.
하지만 코틀린에서는 이런 타입소거를 사용하지 않고 타입 파라미터를 실체화하는 방법이 존재한다.
인라인 함수와 reified 키워드를 사용하면 된다.
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
위에 코드에 inline과 reified를 제거하면 컴파일 에러가 발생한다.
이러한 방법이 작동하는 원리는 컴파일러가 인라인 함수 호출 부분에 해당 코드를 실체화해주기 때문에 인라인 함수에 사용된 타입 파라미터도 실체화가 된다. 따라서 타입이 지정되고 추론이 가능해지면서 해당 코드가 동작한다.
위에서 예를 들었던 List< Type> 의 경우 여러 리스트가 Any로 바뀌어 동작한다 했다.