Let’s think for a moment about everything that you can do with a class name in Kotlin - think of all the cases where you literally type out the name of a class in your source code. I came up with the following 15 cases, but I probably missed a few. Get your scrolling finger ready, here we go…
|
|
2. Function argument: |
|
3. Type argument: |
|
4. Type parameter constraint: |
|
5. Cast to the type: |
|
6. Define the class: |
|
7. Extend the class: |
|
8. Import the class: |
|
9. Reference it in a type alias: |
|
10. Construct an object: |
|
11. Catch an exception type: |
|
12. Invoke a static: |
|
13. Unbound function reference: |
|
14. Compare types: |
|
15. Assign a Class object: |
|
Whew, okay… glad we stopped at 15…
The Big Question
Now, the big question is this:
In which of these cases can we use a generic type parameter reference instead of the real class name?
In other words, where can we replace the word Thing
above with a type parameter, like T
?
Where Can You Reference Type Parameters?
What did you come up with?
Here’s what I found - Cases 1-5 above can all take a type parameter. Let’s demonstrate all five in one go:
class GenericThing<T>(constructorArg: T) {
// 1. Define a member property
private val thing: T = constructorArg
// 2. Define the type of a function argument
fun doSomething(thing: T) = println(thing)
// 3. Use as a type argument
fun emptyList() = listOf<T>()
// 4. Use as a type parameter constraint, and...
// 5. Cast to the type (produces "unchecked cast" warning)
fun <U : T> castIt(): T = thing as U
}
Where Can You Not Reference Type Parameters?
That covers cases 1-5. But for cases 6-15, if you were to substitute a type parameter (e.g., T
) everywhere that we’ve got the literal class name (e.g., Thing
or ExceptionalThing
), you’d end up with a compiler error.
-
In some of these cases, it just wouldn’t make sense to use a type parameter. For example, in case 6 we’re defining a class - what would it even mean to define a class in terms of one of its type parameters?
-
In other cases, Java and Kotlin provide some workarounds via reflection, such as for constructing an object (Case 10).
But in some of these cases, it sure would be nice to be able to use a type parameter. In particular, cases 14 and 15 - comparing types and assigning class objects - would be quite handy in certain situations.
The good news is that reified type parameters allows cases 14 and 15 to compile!
Introducing Reified Type Parameters
Java has limits on what types are considered reifiable - meaning they’re “completely available at run time” (see the Java SE specs on reifiable types). Type parameters are typically erased during compilation, but in the case of reified type parameters in Kotlin, thanks to some fascinating tricks under the hood, you can compare types and get Class objects.
Reified type parameters only work with functions (or extension properties that have a get()
function), and only with those functions that are declared to be inline. Here’s an example:
inline fun <reified T> Any.isInstanceOf(): Boolean = this is T
When you mark a function inline
, the body of the function is simply “pasted” into the call sites. This is why reified types work - the actual type is known at the call site. So invoking x.isInstanceOf<String>()
effectively compiles down to just x is String
. (Or in Java terms, that’s x instanceof String
).
Our Favorite Use Case
Case 15 above is the one that many Kotlin developers love the most. Let’s say we’ve got a User
class, and a JSON string that we want to read from:
data class User(val first: String, val last: String)
val json = """{
"first": "Sherlock",
"last": "Holmes"
}"""
In Java serialization libraries, such as Gson, when you want to deserialize that JSON string, you end up having to pass a Class
object along as an argument, so that Gson knows the type you want.
User user = new Gson().fromJson(getJson(), User.class);
Now, let’s whip together a little reified magic - we’ll create a very light extension function to wrap that Gson method:
inline fun <reified T> Gson.fromJson(json: String) =
fromJson(json, T::class.java)
And now, in our Kotlin code, we can deserialize the JSON string, without even having to pass the type information at all!
val user: User = Gson().fromJson(json)
Kotlin inferred the type based on it’s usage - because we assign it to a variable of type User
, Kotlin used that as the type argument to fromJson()
. Alternatively, you can make the type inference go the other direction:
val user = Gson().fromJson<User>(json)
In this case, the type of user
is inferred from the type argument passed to fromJson()
.
Getting Even More Real
If you’re thirsty for more, check out the concept article about Reified Type Parameters. You can also read the official Kotlin reference on the topic.