Post

(Kotlin) as와 타입 캐스팅. 런타임 에러. 타입 파라미터 소거(erasure)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
fun typeCastTest() {
    val stringMap = mapOf(
        "a" to "0",
        "b" to "10.01"
    )
    val bigDecimalMap = stringMap as Map<String, BigDecimal>
    println(bigDecimalMap)  /* 문제 없이 실행된다. */
    bigDecimalMap.forEach {
        val bigDecimal: BigDecimal = it.value
        println(bigDecimal)
        /* 컴파일은 잘 되지만.. */
        /* 런타임에 class java.lang.String cannot be cast to class java.math.BigDecimal 에러 발생한다 */
    }
}

즉, bigDecimal는 BigDecimal 타입이고 it.value는 String 타입이다. 헌데 컴파일 타임에 문제가 발생하지 않는다. 캐스팅도 잘 되는 것 처럼 보이는데 실제로 사용하는 부분에서 Exception이 발생한다.

왜 그럴까?

JVM의 제네릭스는 보통 타입 소거(type erasure)를 사용해 구현되기 때문에, 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 없다.

예를 들어 List<String> 객체를 만들고 그 안에 문자열이 들어있더라도, 런타임에는 그 객체를 오직 List로만 인식할 수 있다. * 원소를 하나 얻어서 타입 검사를 수행할 수 있겠지만 여러 원소가 서로 다른 타입일 수도 있기 때문에 좋은 방법이 아니다. * 일반적인 경우 List<String>에는 문자열만 들어있음을 가정할 수 있는 이유는

컴파일 타임에 컴파일러가 타입 인자를 인식해 올바른 타입의 값만 리스트에 넣도록 보장 해주기 때문이다.

런타임에는 타입 인자 정보가 없기 때문에, 강제로 다른 타입 인자로 캐스팅해도 캐스팅이 성공한다. 따라서 어떤 경우에는 문제 없이 동작하지만, 또 어떤 경우에는 CastException이 발생하게 된다.

1
2
3
4
5
6
7
8
9
fun listSum(li: List<*>) {
    val intList = li as List<Int>    // 캐스팅은 성공하고
    println(intList.sum())    // 여기서 런타임 Exception 발생
}
>>> listSum(listOf(1, 2, 3))
6
>>> listSum(listOf("a", "b"))
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

* 위와 같은 예제는 <T: Int>로 타입 상한을 지정해서 처리해도 되는 문제이기 때문에 그렇게 처리하는 편이 더 좋다.

그럼 어떻게?

as 캐스팅으로 껍데기만 바꾸지 말고, ObjectMapper 사용해서 본질까지 변환하면 된다.

[Kotlin] 제네릭 : 타입 파라미터 소거(erasure), inline 실체화(reified)

This post is licensed under CC BY 4.0 by the author.