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

Type erasure and reified in Kotlin

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".

A software developer from Vietnam and is currently living in Japan.

One Thought on “Type erasure and reified in Kotlin”

Leave a Reply