Generics in Kotlin provide a powerful way to write reusable and type-safe code. However, on the Java Virtual Machine (JVM), generics are subject to type erasure, meaning that the specific type arguments used for instances of a generic class are not preserved at runtime. This limitation has implications for runtime type checks and casts. But fear not! Kotlin provides a solution: reified type parameters. In this blog post, we’ll delve into the world of reified type parameters and explore how they enable us to access and manipulate type information at runtime.
Understanding Type Erasure in Kotlin Generics
Generics in Kotlin are implemented using type erasure on the JVM. This means that the specific type arguments used for instances of a generic class are not preserved at runtime. In this section, we’ll explore the practical consequences of type erasure in Kotlin and learn how you can overcome its limitations by declaring a function as inline.
By declaring a function as inline, you can prevent the erasure of its type arguments. In Kotlin, this is achieved by using reified type parameters. Reified type parameters allow you to access and manipulate the actual type information of the generic arguments at runtime.
In simpler terms, when you mark a function as inline with a reified type parameter, you can retrieve and work with the specific types used as arguments when calling that function.
Now, let’s look at some examples to better understand the concept of reified type parameters and their usefulness.
Generics at runtime: type checks and casts
Generics in Kotlin, similar to Java, are erased at runtime. This means that the type arguments used to create an instance of a generic class are not preserved at runtime. For example, if you create a List<String>
and put strings into it, at runtime, you will only see it as a List
. You won’t be able to identify the specific type of elements the list was intended to contain. However, the compiler ensures that only elements of the correct type are stored in the list based on the type arguments provided during compilation.
Let’s consider the following code:
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)
Even though the compiler recognizes list1
and list2
as distinct types, at execution time, they appear the same. However, you can generally rely on List<String>
to contain only strings and List<Int>
to contain only integers because the compiler knows the type arguments and enforces type safety. It is possible to deceive the compiler using type casts or Java raw types, but it requires a deliberate effort.
When it comes to checking the type information at runtime, the erased type information poses some limitations. You cannot directly check if a value is an instance of a specific erased type with type arguments. For example, the following code won’t compile:
if (value is List<String>) { ... } // Error: Cannot check for instance of erased type
Even though you can determine at runtime that value
is a List
, you cannot determine whether it’s a list of strings, persons, or some other type. That information is erased.
Note that erasing generic type information has its benefits: the overall amount of memory used by your application is smaller; because less type information needs to be saved in memory.
As we stated earlier, Kotlin doesn’t let you use a generic type without specifying type arguments. Thus you may wonder how to check that the value is a list, rather than a set or another object
To check if a value is a List
without specifying its type argument, you can use the star projection syntax:
if (value is List<*>) { ... }
By using List<*>
, you’re essentially treating it as a type with unknown type arguments, similar to Java’s List<?>
. In this case, you can determine that the value is a List
, but you won’t have any information about its element type.
Note that you can still use normal generic types in as
and as?
casts. However, these casts won’t fail if the class has the correct base type but a wrong type argument because the type argument is not known at runtime. The compiler will emit an “unchecked cast” warning for such casts. It’s important to understand that it’s only a warning, and you can still use the value as if it had the necessary type.
Here’s an example of using as?
cast with a warning:
fun printSum(c: Collection<*>) {
val intList =
c as? List<Int> // Warning here. Unchecked cast: List<*> to List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
This code defines a function called printSum
that takes a collection (c
) as a parameter. Within the function, a cast is performed using the as?
operator, attempting to cast c
as a List<Int>
. If the cast succeeds, the resulting value is assigned to the variable intList
. However, if the cast fails (i.e., c
is not a List<Int>
), the as?
operator returns null
, and the code throws an IllegalArgumentException
with the message “List is expected”. Finally, the sum of the integers in intList
is printed.
Let’s see how this function behaves when called with different inputs:
printSum(listOf(1, 2, 3)) // o/p - 6
When called with a list of integers, the function works as expected. The sum of the integers is calculated and printed.
Now let’s change the input to a set:
printSum(setOf(1, 2, 3)) // o/p - IllegalArgumentException: List is expected
When called with a set of integers, the function throws an IllegalArgumentException
because the input is not a List
. The as?
cast fails, resulting in a null
value, and the IllegalArgumentException
is thrown.
Now we pass String as input:
printSum(listOf("a", "b", "c")) // o/p - ClassCastException: String cannot be cast to Number
When called with a list of strings, the function successfully casts the list to a List<Int>
, despite the wrong type argument. However, during the execution of intList.sum()
, a ClassCastException
occurs. This happens because the function tries to treat the strings as numbers, resulting in a runtime error.
The code examples above demonstrate that type casts (as
and as?
) in Kotlin may lead to runtime exceptions if the casted type and the actual type are incompatible. The compiler emits an “unchecked cast” warning to notify you about this potential risk. It’s important to understand the meaning of these warnings and be cautious when using type casts.
The code snippet below shows an alternative approach using an is
check:
fun printSum(c: Collection<*>) {
val intList =
c as? List<Int> // Warning here. Unchecked cast: List<*> to List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
In this example, the printSum
function takes a Collection<Int>
as a parameter. Using the is
operator, it checks if c
is a List<Int>
. If the check succeeds, the sum of the integers in the list is printed. This approach is possible because the compiler knows at compile time that c
is a collection of integers.
So, Kotlin’s compiler helps you identify potentially dangerous type checks (forbidding
is
checks) and emits warnings for type casts (as
andas?
) that may cause issues at runtime. Understanding these warnings and knowing which operations are safe is essential when working with type casts in Kotlin.
Power of Reified Type Parameters in Inline Functions
In Kotlin, generics are typically erased at runtime, which means that you can’t determine the type arguments used when an instance of a generic class is created or when a generic function is called. However, there is an exception to this limitation when it comes to inline functions. By marking a function as inline, you can make its type parameters reified, which allows you to refer to the actual type arguments at runtime.
Let’s take a look at an example to illustrate this. Suppose we have a generic function called isA
that checks if a given value is an instance of a specific type T
:
fun <T> isA(value: Any) = value is T
If we try to call this function with a specific type argument, like isA<String>("abc")
, we would encounter an error because the type argument T
is erased at runtime.
However, if we modify the function to be inline and mark the type parameter as reified, like this:
inline fun <reified T> isA(value: Any) = value is T
Now we can call isA<String>("abc")
and isA<String>(123)
without any errors. The reified type parameter allows us to check whether the value
is an instance of T
at runtime. In the first example, the output will be true
because "abc"
is indeed a String
, while in the second example, the output will be false
because 123
is not a String
.
Another practical use of reified type parameters is demonstrated by the filterIsInstance
function from the Kotlin standard library. This function takes a collection and selects instances of a specified class, returning only those instances. For example:
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
In this case, we specify <String>
as the type argument for filterIsInstance
, indicating that we are interested in selecting only strings from the items
list. The function’s return type is automatically inferred as List<String>
, and the output will be [one, three]
.
Here’s a simplified version of the filterIsInstance
function’s declaration from the Kotlin standard library:
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
Before coming to this code explanation, have you ever thought, Why reification works for inline functions only? How does this work? Why are you allowed to write element is T in an inline function but not in a regular class or function? Let’s see the answers to all these questions:
Reification works for inline functions because the compiler inserts the bytecode implementing the inline function directly at every place where it is called. This means that the compiler knows the exact type used as the type argument in each specific call to the inline function.
When you call an inline function with a reified type parameter, the compiler can generate a bytecode that references the specific class used as the type argument for that particular call. For example, in the case of the filterIsInstance<String>()
call, the generated code would be equivalent to:
for (element in this) {
if (element is String) {
destination.add(element)
}
}
The generated bytecode references the specific String
class, not a type parameter, so it is not affected by the type-argument erasure that occurs at runtime. This allows the reified type parameter to be used for type checks and other operations at runtime.
It’s important to note that inline functions with reified type parameters cannot be called from Java code. Regular inline functions are accessible to Java as regular functions, meaning they can be called but are not inlined. However, functions with reified type parameters require additional processing to substitute the type argument values into the bytecode, and therefore they must always be inlined. This makes it impossible to call them in a regular way, as Java code does not support this mechanism.
Also, one more thing to note is that an inline function can have multiple reified type parameters and can also have non-reified type parameters alongside the reified ones. It’s important to keep in mind that marking a function as inline does not necessarily provide performance benefits in all cases. If the function becomes large, it’s recommended to extract the code that doesn’t depend on reified type parameters into separate non-inline functions for better performance.
Practical use cases of reified type parameters
Reified type parameters can be especially useful when working with APIs that expect parameters of type java.lang.Class
. Let\’s explore two examples to demonstrate how reified type parameters simplify such scenarios.
Example 1
ServiceLoader The ServiceLoader
API from the JDK is an example of an API that takes a java.lang.Class
representing an interface or abstract class and returns an instance of a service class implementing that interface. Traditionally, in Kotlin, you would use the following syntax to load a service:
val serviceImpl = ServiceLoader.load(Service::class.java)
However, using a function with a reified type parameter, we can make this code shorter and more readable:
val serviceImpl = loadService<Service>()
To define the loadService
function, we use the inline
modifier and a reified type parameter:
inline fun <reified T> loadService(): T {
return ServiceLoader.load(T::class.java)
}
Here, T::class.java
retrieves the java.lang.Class
corresponding to the class specified as the type parameter, allowing us to use it as needed. This approach simplifies the code by specifying the class as a type argument, which is shorter and easier to read compared to ::class.java
syntax.
Example 2
Simplifying startActivity
in Android In Android development, when launching activities, instead of passing the class of the activity as a java.lang.Class
, you can use a reified type parameter to make the code more concise. For instance:
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
With this inline function, you can start an activity by specifying the activity class as a type argument:
startActivity<DetailActivity>()
This simplifies the code by eliminating the need to pass the activity class as a java.lang.Class
instance explicitly.
Reified type parameters allow us to work with class references directly, making the code more readable and concise. They are particularly useful in scenarios where APIs expect java.lang.Class
parameters, such as ServiceLoader in Java or starting activities in Android.
Restrictions on Reified Type Parameters
Reified Type parameters in Kotlin have certain restrictions that you need to be aware of. Some of these restrictions are inherent to the concept itself, while others are determined by the implementation of Kotlin and may change in future Kotlin versions. Here’s a summary of how you can use reified type parameters and what you cannot do:
You can use a reified type parameter in the following ways:
Type checks and casts (is, !is, as, as?)
Reified type parameters can be used in type checks and casts. You can check if an object is of a specific type or perform a type cast using the reified type parameter. Here’s an example:
inline fun <reified T> checkType(obj: Any) {
if (obj is T) {
println("Object is of type T")
} else {
println("Object is not of type T")
}
val castedObj = obj as? T
// Perform operations with the casted object
}
Kotlin reflection APIs (::class)
Reified type parameters can be used with Kotlin reflection APIs, such as ::class
, to access runtime information about the type. It allows you to retrieve the KClass
object representing the type parameter. Here’s an example:
inline fun <reified T> getTypeInformation() {
val typeInfo = T::class
println("Type information: $typeInfo")
}
Getting the corresponding java.lang.Class (::class.java)
Reified type parameters can also be used to obtain the corresponding java.lang.Class
object of the type using the ::class.java
syntax. This can be useful when interoperating with Java APIs that require Class
objects. Here’s an example:
inline fun <reified T> getJavaClass() {
val javaClass = T::class.java
println("Java class: $javaClass")
}
Using reified type parameter as a type argument when calling other functions
Reified type parameters can be used as type arguments when calling other functions. This allows you to propagate the type information to other functions without losing it due to type erasure. Here’s an example:
inline fun <reified T> processList(list: List<T>) {
// Process the list of type T
for (item in list) {
// ...
}
}
fun main() {
val myList = listOf("Hello", "World")
processList<String>(myList)
}
These examples demonstrate the various ways in which reified type parameters can be utilized in Kotlin, including type checks, reflection APIs, obtaining java.lang.Class
, and passing the type information to other functions as type arguments.
However, there are certain things you cannot do with reified type parameters:
Creating new instances of the class specified as a type parameter
Reified type parameters cannot be used to create new instances of the class directly. You can only access the type information using reified type parameters. To create new instances, you would need to use other means such as reflection or factory methods. Here’s an example:
inline fun <reified T> createInstance(): T {
// Error: Cannot create an instance of the type parameter T
return T()
}
Calling methods on the companion object of the type parameter class
Reified type parameters cannot directly access the companion object of the type parameter class. However, you can access the class itself using T::class
syntax. To call methods on the companion object, you would need to access it through the class reference. Here’s an example:
inline fun <reified T> callCompanionMethod(): String {
// Error: Cannot access the companion object of the type parameter T
return T.Companion.someMethod()
}
Using a non-reified type parameter as a type argument
When calling a function with a reified type parameter, you cannot use a non-reified type parameter as a type argument. Reified type parameters can only be used as type arguments themselves. Here’s an example:
inline fun <reified T> reifiedFunction() {
// Error: Non-reified type parameter cannot be used as a type argument
anotherFunction<T>()
}
fun <T> anotherFunction() {
// ...
}
Marking type parameters of classes, properties, or non-inline functions as reified
Reified type parameters can only be used in inline functions. You cannot mark type parameters of classes, properties, or non-inline functions as reified. Reified type parameters are limited to inline functions. Here’s an example:
class MyClass<T> { // Error: Type parameter cannot be marked as reified
// ...
}
val <T> List<T>.property: T // Error: Type parameter cannot be marked as reified
get() = TODO()
fun <T> nonInlineFunction() { // Error: Type parameter cannot be marked as reified
// ...
}
These examples illustrate the restrictions on reified type parameters in Kotlin. By understanding these limitations, you can use reified type parameters effectively in inline functions while keeping in mind their specific usage scenarios.
Conclusion
Reified type parameters in Kotlin offer a powerful tool for overcoming the limitations of type erasure at runtime. By utilizing reified type parameters in inline functions, developers can access and manipulate precise type information, enabling type checks, casts, and interaction with reflection APIs. Understanding the benefits and restrictions of reified type parameters empowers Kotlin developers to write more expressive, type-safe, and concise code.
By embracing reified type parameters, Kotlin programmers can unleash the full potential of generics and enhance their runtime type-related operations. Start utilizing reified type parameters today and unlock a world of type-aware programming in Kotlin!