Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/type-erasure-reified-vie
Generic is a common feature in modern programming languages. As a relatively new language, Kotlin also supports generic typing. However, due to my background in C# .NET, the way Kotlin handles type information at runtime surprised me. Today, we will take a look at type erasure and reified in Kotlin.
What is type erasure?
C# allows us to access type information at runtime
In C#, we can create a Type
instance of a generic type with the typeof
keyword. Below is a simple generic method to print the name of the type used as argument.
public string getInfo<T>(T input) =>
$"Type of <{input}> is: {typeof(T).AssemblyQualifiedName}" ?? "Cannot get type name";
We can verify that the method above is working as expected.
Console.WriteLine(getInfo(17));
// Type of <1> is: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
Console.WriteLine("This is a text");
// Type of <This is a text> is: System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
But we cannot access type information at runtime with Kotlin
However, a similar method in Kotlin won’t even compile.
fun <T> getInfo(input: T): String =
"The type of <$input> is: ${T::class.qualifiedName}"
Instead, it results in the following compiler error.
Cannot use 'T' as reified type parameter. Use a class instead.
Type erasure in Kotlin
Similar to Java, Kotlin also has type erasure. This feature does exactly what it says. At runtime, information about the type argument in a generic method is not preserved. This is the reason for the compiler error above. At runtime, the JVM has no information about the generic type T
. Because of that, it cannot create a KClass
instance, and cannot get the type name.
Let’s modify getInfo
and remove the part accessing type information.
fun <T> getInfo2(input: T): String = "Cannot get type name"
We can compile and call this function with no errors.
getInfo2(17) // The result is "Cannot get type name"
Now let’s take a look at the generated bytecode of getInfo2
(from IntelliJ, select Tools -> Kotlin -> Show Kotlin bytecode). If you try this experiment yourself, keep in mind that your line number might be different.
public final static getInfo2(Ljava/lang/Object;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;()
L0
LINENUMBER 184 L0
LDC "Cannot get type name"
ARETURN
L1
LOCALVARIABLE input Ljava/lang/Object; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
As we can see, in the bytecode, the type of input
is simply Ljava/lang/Object
. The JVM has no idea that it is actually an integer. And at the call site, we simply invoke the getInfo2
method like this.
L0
LINENUMBER 15 L0
BIPUSH 17
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKESTATIC org/hellokotlin/MainKt.getInfo2 (Ljava/lang/Object;)Ljava/lang/String; // <-- Invoke getInfo2
POP
Keep type information with the reified keyword
Inline function with reified generic types
Is there any way to access type information at runtime? Actually, that is possible with the reified
keyword inside an inline function. The function below can be compiled without error.
inline fun <reified T> getInfoFix(input: T): String =
"The type of <$input> is: ${T::class.qualifiedName}"
And it also works as we expected.
println(getInfoFix(17))
// The type of <17> is: kotlin.Int
How does a reified type work?
Let’s take a look at the bytecode of the getInfoFix
function above (again, your line number might be different).
public final static synthetic getInfoFix(Ljava/lang/Object;)Ljava/lang/String;
L0
LDC 0
ISTORE 1
L1
LINENUMBER 181 L1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "The type of <"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC "> is: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ICONST_4
LDC "T"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.reifiedOperationMarker (ILjava/lang/String;)V
LDC Ljava/lang/Object;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE input Ljava/lang/Object; L0 L2 0
LOCALVARIABLE $i$f$getInfo I L1 L2 1
MAXSTACK = 3
MAXLOCALS = 2
This bytecode is quite long. But the interesting part is here, where we get the class name of T
.
LDC Ljava/lang/Object;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
At first glance, we still use Ljava/lang/Object
like in the non-reified version. But keep in mind that our function is also inlined
.
Inlining a generic function with reified types
Here is the bytecode to call getInfoFix
.
L0
LINENUMBER 15 L0
BIPUSH 17
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 0
L1
ICONST_0
ISTORE 1
L2
LINENUMBER 704 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "The type of <"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC "> is: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC Ljava/lang/Integer;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
L3
POP
Because our function is inlined, instead of invoking getInfoFix
, its bytecode is substituted directly into the call site. And at the call site, we know the actual type of T
. The three lines in the previous section now become these.
LDC Ljava/lang/Integer;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
We can clearly see that it loaded Ljava/lang/Integer;.class
on the stack. And this is why getInfoFix
can access type information and get its name at runtime.
Conclusion
While getting familiar with Kotlin/Java, I try to rely on my experience with C# .NET. Unfortunately, sometimes there are subtle differences between those languages. On one hand, they can trip me up. But on the other hand, I consider them excellent opportunities to dive a little deeper into the new language.
Most of the time, I consider accessing the type information at runtime to be a code smell. But if it’s really necessary, I’ll inline the function and use the reified
keyword. After all, "only a Sith deals in absolutes".
One Thought on “Type erasure and reified in Kotlin”