Skip to main content

Kotlin Type Erasure

Introduction

When working with generics in Kotlin, you might encounter situations where the type information you expect to be available at runtime is missing. This happens because of a concept called type erasure. Type erasure is a mechanism used by the JVM (Java Virtual Machine) where generic type information is removed during compilation. This means that at runtime, a List<String> and a List<Int> are represented as the same type: just List.

In this article, we'll explore what type erasure is, why it exists, its implications for Kotlin programming, and how to work around its limitations.

What is Type Erasure?

Type erasure refers to the process by which generic type information is removed during compilation. This means that at runtime, the JVM doesn't have information about the specific type arguments used in generic classes or functions.

Let's look at a simple example:

kotlin
fun <T> printList(list: List<T>) {
println("This is a list of ${list::class.java}")
}

fun main() {
val stringList = listOf("apple", "banana", "cherry")
val intList = listOf(1, 2, 3)

printList(stringList) // Output: This is a list of class java.util.Arrays$ArrayList
printList(intList) // Output: This is a list of class java.util.Arrays$ArrayList
}

As you can see, both lists have the same class at runtime. The information that one is a list of strings and the other is a list of integers has been "erased."

Why Type Erasure Exists

Type erasure was introduced in Java (and thus inherited by Kotlin) to ensure backward compatibility when generics were added to the language. This decision allows for:

  1. Compatibility with older code: Pre-generic code can work with generic code without modification.

  2. Smaller runtime footprint: Without creating different versions of a class for each type parameter, the JVM needs less memory to represent classes.

Implications of Type Erasure in Kotlin

1. Runtime Type Checking Limitations

One of the most significant implications is the inability to check for specific generic types at runtime:

kotlin
fun main() {
val list = listOf("Hello", "World")

if (list is List<String>) { // Warning: Cannot check for instance of erased type: List<String>
println("This is a list of strings")
}

// This is allowed, but not very useful since all Lists will match
if (list is List<*>) {
println("This is a list of something")
}
}

2. Class Literals are Not Available for Generic Types

You cannot use class literals for generic types directly:

kotlin
// This won't compile
val stringListClass = List<String>::class.java

// This works but gives you the raw List type
val listClass = List::class.java

3. Unchecked Casts

Sometimes, you need to perform casts that the compiler cannot verify due to type erasure:

kotlin
fun <T> convertToList(obj: Any): List<T> {
// This will generate an "unchecked cast" warning
@Suppress("UNCHECKED_CAST")
return obj as List<T>
}

Working Around Type Erasure in Kotlin

1. Using reified Type Parameters with Inline Functions

Kotlin provides a powerful feature called reified type parameters when used with inline functions. This allows you to access the actual type arguments at runtime:

kotlin
inline fun <reified T> isListOfType(list: List<*>): Boolean {
return list.all { it is T }
}

fun main() {
val stringList = listOf("Hello", "World")
val mixedList = listOf("Hello", 42)

println(isListOfType<String>(stringList)) // Output: true
println(isListOfType<String>(mixedList)) // Output: false
}

The reified keyword preserves the type information at the call site by inlining the function, essentially generating specialized code for each type used.

2. Using KClass Parameters

You can explicitly pass class objects to functions:

kotlin
fun <T : Any> checkType(list: List<*>, klass: KClass<T>): Boolean {
return list.all { klass.isInstance(it) }
}

fun main() {
val stringList = listOf("Hello", "World")

println(checkType(stringList, String::class)) // Output: true
println(checkType(stringList, Int::class)) // Output: false
}

3. Type Tokens with @JvmSuppressWildcards

For more advanced scenarios, especially when interacting with Java code, you might need to use type tokens:

kotlin
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.reflect.KClass

abstract class TypeToken<T> {
val type: Type
get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}

inline fun <reified T> typeToken(): Type = object : TypeToken<T>() {}.type

fun main() {
val stringListType = typeToken<List<String>>()
println(stringListType) // Output: java.util.List<java.lang.String>
}

Real-World Application Example

Let's look at a practical example of dealing with type erasure when working with a JSON parsing library:

kotlin
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.lang.reflect.Type

// Our JSON parser utility
class JsonParser {
private val mapper = ObjectMapper().registerKotlinModule()

// Using reified type parameter
inline fun <reified T> parse(json: String): T {
return mapper.readValue(json, T::class.java)
}

// Using TypeToken for complex generic types
fun <T> parseGeneric(json: String, type: Type): T {
return mapper.readValue(json, mapper.typeFactory.constructType(type))
}
}

// Data classes for our example
data class User(val name: String, val age: Int)
data class GenericResponse<T>(val status: String, val data: T)

fun main() {
val jsonParser = JsonParser()
val userJson = """{"name":"John Doe","age":30}"""

// Simple case using reified
val user: User = jsonParser.parse(userJson)
println(user) // Output: User(name=John Doe, age=30)

// Complex generic case using TypeToken
val responseJson = """{"status":"success","data":{"name":"John Doe","age":30}}"""
val response: GenericResponse<User> = jsonParser.parseGeneric(
responseJson,
typeToken<GenericResponse<User>>()
)
println(response) // Output: GenericResponse(status=success, data=User(name=John Doe, age=30))
}

This example shows how you might work around type erasure when working with JSON libraries that need to deserialize complex generic types.

Summary

Type erasure is a fundamental aspect of how generics work in Kotlin (inherited from Java's implementation). While it has some limitations, Kotlin provides several ways to work around these limitations:

  1. Inline functions with reified type parameters allow you to access type information at runtime.
  2. Passing class references explicitly can help when you need to check types.
  3. Type tokens can be used for more complex generic type handling.

Understanding type erasure and its workarounds is essential when working with generics in Kotlin, especially when you need to perform type-dependent operations at runtime.

Additional Resources

Exercises

  1. Create a function that takes a list of any type and prints only elements of a specific type.
  2. Implement a generic cache that can store and retrieve different types of objects while preserving their specific types.
  3. Write a function that can convert a JSON array to a strongly typed List<T> without warnings or unchecked casts.
  4. Create your own implementation of a type-safe heterogeneous container that works around type erasure.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)