How does the reified keyword in Kotlin work?
I'm trying to understand the purpose of the reified
keyword, apparently it's allowing us to do reflection on generics.
However, when I leave it out it works just as fine. Anyone care to explain when this makes an actual difference?
TL;DR: What is reified
good for
fun <T> myGenericFun(c: Class<T>)
In the body of a generic function like myGenericFun
, you can't access the type T
because it's only available at compile time but erased at runtime. Therefore, if you want to use the generic type as a normal class in the function body you need to explicitly pass the class as a parameter as shown in myGenericFun
.
If you create an inline
function with a reified T
, the type of T
can be accessed even at runtime, and thus you do not need to pass the Class<T>
additionally. You can work with T
as if it was a normal class - e.g. you might want to check whether a variable is an instance of T
, which you can easily do then: myVar is T
.
Such an inline
function with reified
type T
looks as follows:
inline fun <reified T> myGenericFun()
How reified
works
You can only use reified
in combination with an inline
function. By doing so, you instruct the compiler to copy the function's bytecode to every spot the function is invoked from (the compiler "inlines" the function). When you call an inline
function with reified
type, the compiler has to be able to know the actual type passed as a type argument so that it can modify the generated bytecode to use the corresponding class directly. Therefore a call like myVar is T
becomes myVar is String
in the bytecode (if the type argument is String
).
Example
Let's have a look at an example that shows how helpful reified
can be.
We want to create an extension function for String
called toKotlinObject
that tries to convert a JSON string to a plain Kotlin object with a type specified by the function's generic type T
. We can use com.fasterxml.jackson.module.kotlin
for this and the first approach is the following:
a) First approach without reified type
fun <T> String.toKotlinObject(): T {
val mapper = jacksonObjectMapper()
//does not compile!
return mapper.readValue(this, T::class.java)
}
The readValue
method takes a type that it is supposed to parse the JsonObject
to. If we try to get the Class
of the type parameter T
, the compiler complains: "Cannot use 'T' as reified type parameter. Use a class instead."
b) Workaround with explicit Class
parameter
fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, c.java)
}
As a workaround, the Class
of T
can be made a method parameter, which then used as an argument to readValue
. This works and is a common pattern in generic Java code. It can be called as follows:
data class MyJsonType(val name: String)
val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)
c) The Kotlin way: reified
Using an inline
function with reified
type parameter T
makes it possible to implement the function differently:
inline fun <reified T: Any> String.toKotlinObject(): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, T::class.java)
}
There’s no need to take the Class
of T
additionally, T
can be used as if it was an ordinary class. For the client the code looks like this:
json.toKotlinObject<MyJsonType>()
Important Note: Working with Java
An inlined function with reified
type is not callable from Java code.
Understanding reified
types
Generics
While using generics in Kotlin, we can perform operations on a value of any type T
:
fun <T> doSomething(value: T) {
println("Doing something with value: $value") // OK
}
Here we are implicitly calling the toString()
function of the value
and that works.
But we can't perform any operations on the type T
directly:
fun <T> doSomething(value: T) {
println("Doing something with type: ${T::class.simpleName}") // Error
}
Let's understand the reason for this error.
Type erasure
In the code above, the compiler gives an error: Cannot use 'T' as reified type parameter. Use a class instead.
This happens because at compile time, the compiler removes the type argument from the function call.
For example, if you call the function as:
doSomething<String>("Some String")
The compiler removes the type argument part <String>
and all that's left at the runtime is:
doSomething("Some String")
This is called type erasure. So, at runtime (inside the function definition), we cannot possibly know exactly which type the T
stands for.
Java solution
The solution to this type erasure problem in Java was to pass an additional argument specifying the type with the Class
(in Java) or KClass
(in Kotlin):
fun <T: Any> doSomething(value: T, type: KClass<T>) {
println("Doing something with type: ${type.simpleName}") // OK
}
This way our code is not affected by type erasure. But this solution is verbose and not very elegant since we have to declare it as well as call it with an additional argument. Also, specifying the type bound Any
is mandatory.
Type reification
The best solution to the problem above is type reification in Kotlin. The reified
modifier before the type parameter enables the type information to be retained at runtime:
inline fun <reified T> doSomething(value: T) {
println("Doing something with type: ${T::class.simpleName}") // OK
}
In the code above, thanks to the reified
type parameter, we no longer get the error while performing an operation on type T
. Let's see how inline
functions make this magic possible.
inline
functions
When we mark a function as inline
, the compiler copies the actual body of that inline
function wherever that function is called. Since we marked our doSomething()
function as an inline
, the following code:
fun main() {
doSomething<String>("Some String")
}
gets compiled to:
fun main() {
println("Doing something with type: ${String::class.simpleName}")
}
So, the two code snippets shown above are equivalent.
While copying the body of an inline
function, the compiler also replaces the type parameter T
with the actual type argument that is specified or inferred in the function call. For example, notice how the type parameter T
is replaced with the actual type argument String
.
Type checking and type casting of reified
types
The main objective of a reified
type parameter is to know the exact type that the type parameter T
represents at runtime.
Let's say we have a list of different types of fruits:
val fruits = listOf(Apple(), Orange(), Banana(), Orange())
And we want to filter all the Orange
types in a separate list like following:
val oranges = listOf(Orange(), Orange())
Without reified
For filtering the fruit types, we may write an extension function on List<Any>
like following:
fun <T> List<Any>.filterFruit(): List<T> {
return this.filter { it is T }.map { it as T } // Error and Warning
}
In this code, first we filter the types and only take the element if its type matches the given type argument. Then we cast each element to the given type argument and return
the List
. But there are two problems.
Type checking
While type checking it is T
, we are introduced to another error by the compiler: Cannot check for instance of erased type: T
. This is another kind of error you may come across due to type erasure.
Type casting
While type casting it as T
, we are also given a warning: Unchecked cast: Any to T
. The compiler cannot confirm the type due to type erasure.
reified
types to the rescue
We can easily overcome these two problems by marking the function as inline
and making the type parameter reified
as explained earlier:
inline fun <reified T> List<Any>.filterFruit(): List<T> {
return this.filter { it is T }.map { it as T }
}
And then call it like following:
val oranges = fruits.filterFruit<Orange>()
I showed this function for easier demonstration. For the purpose of filtering the types in collections, there is already a standard library function filterIsInstance()
. This function has used the inline
and reified
modifiers in the similar manner. You can simply call it as following:
val oranges = fruits.filterIsInstance<Orange>()
Passing reified
parameter as an argument
The reified
modifier makes it possible for a function to pass the type parameter as a type argument to another function that has reified
modifier:
inline fun <reified T> doSomething() {
// Passing T as an argument to another function
doSomethingElse<T>()
}
inline fun <reified T> doSomethingElse() { }
Getting the generic type of the reified
type
Sometimes a type argument can be a generic type. For example, List<String>
in the function call doSomething<List<String>>()
. It's possible to know this entire type, thanks to reification:
inline fun <reified T> getGenericType() {
val type: KType = typeOf<T>()
println(type)
}
Here the typeOf()
is a standard library function. The println()
function above will print kotlin.collections.List<kotlin.String>
, if you call the function as getGenericType<List<String>>()
. The KType
includes KClass
, type argument information and nullability information. Once you know the KType
, you can perform reflection on it.
Java interoperability
The inline
functions declared without reified
type parameters can be called from Java as regular Java functions. But the ones declared with the reified
type parameters are not callable from Java.
Even if you call it using the reflection like following:
Method method = YourFilenameKt.class.getDeclaredMethod("doSomething", Object.class);
method.invoke("hello", Object.class);
You get the UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.
Conclusion
In many cases, the reified
types help us get rid of the following errors and warnings:
Error: Cannot use 'T' as reified type parameter. Use a class instead.
Error: Cannot check for instance of erased type: T
Warning: Unchecked cast: SomeType to T
That's it! Hope that helps understand the essence of reified
types.
reified
is to give permission to use at compilation time (to access T
inside the function).
For example:
inline fun <reified T:Any> String.convertToObject(): T{
val gson = Gson()
return gson.fromJson(this,T::class.java)
}
To use:
val jsonStringResponse = "{"name":"bruno" , "age":"14" , "world":"mars"}"
val userObject = jsonStringResponse.convertToObject<User>()
println(userObject.name)