JVM의 제네릭스는 보통 타입 소거(type erasure)를 사용해 구현된다. 이는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻이다. 이번 절에서는 코틀린 타입 소거가 어떤 영향을 끼치는지 살펴보고 함수를 inline으로 선언함으로써 이런 제약을 어떻게 우회할 수 있는지 살펴본다. 함수를 inline으로 만들어 타입 인자가 지워지지 않게 하는 것을 실체화(reify)라고 부른다.
제네릭 클래스 인스턴스는 그 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않는다. 예를 들어 List<String> 객체를 만들면, 실행 시점에는 그 객체를 오직 List로만 볼 수 있고, 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없다.
컴파일러는 List<String>과 List<Int>, 두 리스트를 서로 다른 타입으로 인식하지만 실행 시점에 그 둘은 완전히 같은 타입의 객체다. 그럼에도 List<String>에는 문자열만 들어있고, List<Int>에는 정수만 들어있다고 가정할 수 있는데, 이는 컴파일러가 타입 인자를 알고 올바른 타입의 값만 각 리스트에 넣도록 보장해주기 때문이다.
타입 소거로 인해, 실행 시점에 타입 인자를 검사할 수 없다. 검사하는 코드를 작성하면 컴파일 시 오류를 발생시킨다.
fun check(value: Any) {
if (value is List<String>) { // Cannot check for instance of erased type: List<String>
print("Yes")
}
}
실행 시점에 어떤 값이 List인지 여부는 확실히 알 수 있지만, 그 리스트에 담긴 값의 타입은 알 수가 없기 때문이다. 다만 저장해야 하는 타입 정보의 크기가 줄어들어서 전반적인 메모리 사용량이 줄어든다는 제네릭 타입 소거 나름의 장점이 있다.
원소의 타입은 알 수 없으나, 어떤 값이 집합인지, Map인지, List인지는 스타 프로젝션(star projection)을 사용해서 알 수 있다.
fun main() {
val value = listOf("Hello", "World")
check(value)
}
fun check(value: Any) {
if (value is List<*>) {
print("Yes List")
}
}
타입 파라미터가 2개 이상이라면 모든 타입 파라미터에 *를 포함시켜야 한다. 스타 프로젝션에 대해서는 9장의 뒤에서 더 자세히 다룬다.
as나 as? 캐스팅에도 여전히 제네릭 타입을 사용할 수 있다. 하지만 기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅해도 여전히 캐스팅에 성공한다는 점을 조심해야 한다. 실행 시점에는 제네릭 타입의 타입 인자를 알 수 없으므로 캐스팅은 항상 성공한다. 그런 타입 캐스팅을 사용하면 컴파일러가 “unchecked cast”라는 경고를 해준다. 하지만 컴파일러는 단순히 경고만 하고 컴파일을 진행하므로 다음 코드처럼 값을 원하는 제네릭 타입으로 캐스팅해 사용해도 된다.
fun main() {
printSum(listOf(1,2,3))
}
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // Unchecked cast: Collection<*> to List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
setOf(1,2,3)과 같은 정수 집합에 대해서는 IllegalArgumentException이 발생한다.
listOf(”a”, “b”, “c”)와 같이 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 ClassCastException이 발생한다.
printSum(listOf("a", "b", "c"))
// java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Number
컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행할 수 있다.
fun printSum(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
제네릭 함수가 호출되도 그 함수의 본문에서는 호출 시 쓰인 타입 인자를 알 수 없다.
fun <T> isA(value: Any) = value is T
// Cannot check for instance of erased type: T