1. Overview

In this tutorial, we’re going to see how reified inline functions can help us to write elegant and concise generic abstractions.

First, we’ll get familiar with type erasure and how it affects the code readability sometimes. Then, we’ll see how Kotlin resolves the issue with reified inline functions.

2. Type Erasure

Kotlin and Java erase generic type information at compile time. That is, the generic type parameters, such as <T>, are merely present in the source code.

Therefore, all possible forms of a generic type manifest themselves as one simple raw type at runtime. For instance, List<Int> and List<String> are both a List at runtime. This behavior is known as the erasure.

Sometimes, erasure will force us to express simple ideas in inelegant fashions. For example, many Java developers are used to writing this to parse a JSON:

String json = objectMapper.readValue(data, String.class);

Adding insult to injury, the super type token pattern makes the code even less readable:

Map<String, String> json = objectMapper.readValue(data, 
  new TypeReference<Map<String, String>>() {});

All of this is because we don’t have access to the class metadata of a generic parameter:

public <T> T readValue(byte[] data) {
    Class<T> type = T.class; // won't compile
}

We can’t use something like T.class in Java because is not reified at runtime. Technically speaking, a type is reified if it’s present at runtime.

Let’s see how Kotlin fixes this limitation.

3. Inline Functions

Inline functions are yet another compile-time illusion. Although they’re present in the source code, the compiler will replace their body in each call site, thereby completely avoiding the function call.

For example:

fun main() {
    printHello()
}

inline fun printHello() {
    print("Hello ")
    println("World")
}

Kotlin’s compiler will replace the printHello function call with its body because it’s marked as inline. Therefore, the runtime representation of the main function is:

fun main() {
    print("Hello ")
    println("World")
}

4. Reified Functions

Inlining introduces an exciting opportunity: reifying the generic type information at runtime. That is, we no longer need to pass type information in the form of T::class as an argument.

All we have to do is to mark the type parameter with the reified keyword:

inline fun <reified T> ObjectMapper.readValue(data: ByteArray): T =
  readValue(data, object : TypeReference<T>() {})

This is possible since Kotlin is going to inline the function and already knows about the call site context.

Consequently, this extension function makes it easy to call the readValue method without explicitly passing the type information:

val json = objectMapper.readValue<String>(data)

Similarly, we can avoid the generic syntax completely:

val json: String = objectMapper.readValue(data)

Additionally, there wouldn’t be any need for super type tokens either as it’s been already encapsulated in the extension function:

val json: Map<String, String> = objectMapper.readValue(data)

5. Bytecode Representation

Now that we know how to use reified functions in Kotlin, let’s see what they look like under-the-hood:

val objectMapper = ObjectMapper()
val data = """{"answer": 42}""".toByteArray()
val json: Map<String, String> = objectMapper.readValue(data)

In order to do that, let’s take a peek at the generated bytecode for the preceding snippet:

// omitted 
35: aload_3
36: aload_1
37: new           #65                 // class MainKt$main$$inlined$readValue$1
40: dup
41: invokespecial #66                 // Method MainKt$main$$inlined$readValue$1."<init>":()V
44: checkcast     #30                 // class TypeReference
47: invokevirtual #35                 // Method ObjectMapper.readValue:([BLTypeReference;)LObject;
50: checkcast     #68                 // class Map
53: astore_2

This piece of bytecode performs the following steps:

  • Loads the JSON byte array and prepares the inline extension function
  • Passes the loaded parameters to the readValue method and calls that method (invokevirtual)
  • Returns an instance of Object (look at what erasure does!)
  • Casts the returned Object to Map because it knows from the context that it should be a Map

Basically, this bytecode is equivalent to the Java code:

Map<String, String> json = objectMapper.readValue(data, 
  new TypeReference<Map<String, String>>() {});

But with the following syntax:

val json: Map<String, String> = objectMapper.readValue(data)

This is a much more pleasant syntax with the exact same internal representation, yet another compiler illusion!

6. Conclusion

In this tutorial, we got familiar with the problems the reified type parameters are going to solve. Then we saw how to use them and how they look under-the-hood.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Pavel Samokha
Pavel Samokha
11 months ago

>Additionally, there wouldn’t be any need for super type tokens either:
 
That’s wrong
You’ll still need type token for Generic Types, cause readValue<T> where T is Map<K,V> after Kotlin’s inline reification and compilation to bytecode will now that T is Map, but now nothing on K and V.
 
That’s why instead implementation presented in the article
 

inline fun <reified T> ObjectMapper.readValue(data: ByteArray): T =
  readValue(data, T::class.java)

 
jackson-kotlin-module has following implementation:
 

inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object : TypeReference<T>() {}

inline fun <reified T> ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
jimmy
jimmy
10 months ago

Thank you, good article

Comments are closed on this article!