Welcome to Kotlin’s series of articles:

Original series:

  • Everything you need to know about the Kotlin property broker
  • Source code parsing for Kotlin Sequences
  • Complete analysis of Sets and functional apis in Kotlin – Part 1
  • Complete parsing of lambdas compiled into bytecode in Kotlin syntax
  • On the complete resolution of Lambda expressions in Kotlin’s Grammar
  • On extension functions in Kotlin’s Grammar
  • A brief introduction to Kotlin’s Grammar article on top-level functions, infix calls, and destruct declarations
  • How to Make functions call Better
  • On variables and Constants in Kotlin’s Grammar
  • Elementary Grammar in Kotlin’s Grammar Essay

Translation series:

  • Kotlin’s trick of Reified Type Parameter
  • When should type parameter constraints be used in Kotlin generics?
  • An easy way to remember Kotlin’s parameters and arguments
  • Should Kotlin define functions or attributes?
  • How to remove all of them from your Kotlin code! (Non-empty assertion)
  • Master Kotlin’s standard library functions: run, with, let, also, and apply
  • All you need to know about Kotlin type aliases
  • Should Sequences or Lists be used in Kotlin?
  • Kotlin’s turtle (List) rabbit (Sequence) race
  • The Effective Kotlin series considers using static factory methods instead of constructors
  • The Effective Kotlin series considers using a builder when encountering multiple constructor parameters

Actual combat series:

  • Use Kotlin to compress images with ImageSlimming.
  • Use Kotlin to create a picture compression plugin.
  • Use Kotlin to compress images.
  • Simple application of custom View picture fillet in Kotlin practice article

Brief description: Today we start the original series of articles, first of all, why not take this as a translation? I read the author’s original text, and this blog will cover everything in it. In this article you will learn all about the Reified parameter in Kotlin generics, including its basic usage, source code, and usage scenarios. With the introduction of kotlin’s Reified type parameter, you now have a good idea of it. This article will take a more complete look at the principle and use of Kotlin’s Reified type parameter. Without further ado, let’s go straight to the chapter map:

Generic type erasure

We learned from the previous article that generics in the JVM are generally implemented by type erase. that is, the type arguments of a generic class instance are erased at compile time and not preserved at run time. There are historical reasons for doing this. One of the biggest reasons is to be compatible with pre-JDK1.5 versions. Of course, generic type erasers are also beneficial, discarding some of the type argument information at runtime and reducing the memory footprint. Because of generic type erasure, Generics in Java are also known as pseudo generics. This is because after compilation all generic type argument types are replaced by Object types or generic type parameters that specify upper bounds on the type of the class. Such as: List

, List

, List

Float, String, Student are all replaced with Object when the JVM runs, If the generic definition is List

, the runtime T is replaced by the Student type, as you can see by reflecting the Erasure class.



There is no historical reason why Kotlin needs to be compatible with older versions of Java, but because the Kotlin compiler compiles classes that run on the same JVM as Java, the JVM’s generics are usually erased by generics, Kotlin has never been able to overcome generic erasures. But Kotlin is a language with a lot of goals and doesn’t want to be yelled at by C# about generic collections that don’t even know their own type arguments, so Kotlin did a little magic with inline inline functions.

What are the effects of generic erasure?

I’m using Kotlin as an example of what generic erasure can do, because Kotlin has to deal with the same problems that Java has. Let’s look at an example

fun main(args: Array<String>) {
    val list1: List<Int> = listOf(1.2.3.4)
    val list2: List<String> = listOf("a"."b"."c"."d")
    println(list1)
    println(list2)
}
Copy the code

The above two collections store elements of type Int and type String respectively, but they are replaced with List primitives in the compiled class file to take a look at the decompiled Java code

@Metadata(
   mv = {1.1.11},
   bv = {1.0.2},
   k = 2,
   d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0 010 \ u0000 \ u001a \ u00020 \ u00012 \ f \ u0010 \ u0002 \ u001a \ b \ u0012 \ u0004 \ u0012 \ u00020 \ u00040 \ u0003 ¢\ u0006 \ u0002 \ u0010 \ u0005 ¨ \ u000 6\u0006"},
   d2 = {"main".""."args"."".""."([Ljava/lang/String;)V"."Lambda_main"})public final class GenericKtKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      List list1 = CollectionsKt.listOf(new Integer[]{1.2.3.4});//List native types
      List list2 = CollectionsKt.listOf(new String[]{"a"."b"."c"."d"});//List native typesSystem.out.println(list1); System.out.println(list2); }}Copy the code

We see that the compiled listOf function accepts Object instead of the concrete String and Int types.

1.Type checking problems:

The is type check in Kotlin does not detect types in type arguments in general, as follows.

if(value isList<String>){... }// Normally such code will not compile
Copy the code

Analysis: Although we can determine at run time that a value is a List, we cannot determine what type of data elements are stored in the List because the type argument types of generic classes are erased and replaced by Object types or upper bound parameter constraint types. But how do you correctly check if a value is a List? See the solution below

Solution in Java: Java has a straightforward solution to the above problem, which is to use the List primitive type.

if(value is List){... }Copy the code

Kotlin’s solution: As we all know, Kotlin doesn’t support java-like primitive types. All generic classes need to display the types of arguments of the specified type. Kotlin can solve this problem by using the star projection List<*>(more on star projection later). It works like a List
wildcard.

if(value isList<*>){... }Copy the code

Special case: We say that is checks generally do not detect type arguments, but there is a special case that is Kotlin’s compiler intelligent inference (I have to admire the Kotlin compiler’s intelligence)

fun printNumberList(collection: Collection<String>) {
    if(collection isList<String>){... }// This is legal here.
}
Copy the code

The Kotlin compiler can intelligently deduce the type of the collection argument based on the current scope context. Because the type argument of the collection function’s generic class is String, the above example can only use String as the type argument.

2,Type conversion problems:

In Kotlin we use as or as? To perform type conversions, note that you can still use generic types when using AS conversions. Only the underlying type of the generic class is correct and even type argument errors will compile normally, but with a warning thrown. Let’s look at an example

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf(1.2.3.4.5))// Pass data of type List
      
}

fun printNumberList(collection: Collection< * >) {
    val numberList = collection as List<Int>// List
      
    println(numberList)
}
Copy the code

Run the output

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a"."b"."c"."d"))// Pass in data of type List
      
}

fun printNumberList(collection: Collection< * >) {
    val numberList = collection as List<Int>
    List
      
        = List
       
      
    // However, it is important to note that the type argument cannot be treated as an Int by default, because erasure cannot determine the current type argument, otherwise runtime exceptions may occur
    println(numberList)
}
Copy the code

Run the output

If we call it setOf(1,2,3,4,5)

fun main(args: Array<String>) {
    printNumberList(setOf(1.2.3.4.5))}fun printNumberList(collection: Collection< * >) {
    val numberList = collection as List<Int>
    println(numberList)
}
Copy the code

Run the output

Analysis: If you think about it, it’s not unusual to get this result. We know that the type arguments of the generic are erased at compile time, but the underlying type of the generic class is not affected. We don’t know what element type List stores, but we do know that this is a List and not a Set, so the latter will definitely throw an exception. As for the former, the type arguments cannot be determined at runtime, but the underlying type can be determined. So whenever the underlying types match, and the type arguments can’t be determined whether they might or might not match, Kotlin compilers throw a warning.

Note: this is not recommended as a security risk, since the compiler only gives warning, not a backstop. The problem happens when the caller passes in an underlying type match and the type argument doesn’t match by default.

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a"."b"."c"."d"))}fun printNumberList(collection: Collection< * >) {
    val numberList = collection as List<Int>
    println(numberList.sum())
}
Copy the code

Run the output

What is reified?

We know that Kotlin and Java also have problems with generic type erasure, but Kotlin, as a modern programming language, knows the problems caused by Java erasure, so he has a backdoor by using inline functions to ensure that the type arguments of a generic class are preserved at runtime. This operation, which Kotlin refers to as ified, requires the reified keyword.

1. Satisfy the necessary conditions for implementing type parameter functions

  • Must be inline functions, decorated with inline keywords
  • A generic class must use the reified keyword when defining a generic parameter

2. Basic function definitions with arguments of the implemented type

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 
Copy the code

For the above example, we can say that the type parameter T is an implementation type parameter of the generic function isInstanceOf.

3. One more thing about inline functions

We are familiar with inline functions. One of the biggest benefits of using inline functions is the performance optimization and improvement of function calls. However, it is important to note that the use of inline functions is not a performance issue, but an additional benefit. And how does it actually materialize you can keep going, right

Fourth, the principle behind the implementation of type parameter functions and decompilation analysis

We know that type implementation parameters are really a syntax trick Kotlin has turned into, so it’s time to demystify the magic. In fact, the key to this magic trick is the inline function, and without the inline function the magic would not work.

1. Principle description

We all know how inline functions work. The compiler dynamically inserts the bytecode that implements the inline function at each call point. This is how it works. Every time a function is called with an argument to the implemented type, the compiler knows what type is used as the argument to the generic type. So the compiler simply generates bytecode that corresponds to a different type of argument call and inserts it into the call point at each call. In short, a function with an argument argument generates bytecodes of different types of arguments each time it is called, dynamically inserted into the call point. Because the type arguments to the generated bytecode refer to specific types, rather than type parameters, there is no erasing problem.

The example of Reified

Functions that take type arguments are widely used in Kotlin development, especially in some of Kotlin’s official libraries. Here’s a simplified version of the startActivity function from the Anko library

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any? >) =
        AnkoInternals.internalStartActivity(this, T::class.java.params)
Copy the code

As you can see from the above example, we define an implementation type parameter T, and it has an upper bound on the type parameter to constrain the Activity. It can directly use the implementation type parameter T as a normal type

3. Code decompilation analysis

I’m going to copy that function out of the library for decompiling purposes and I’m going to call it startActivityKt for purposes of analysis.

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()// This simply launches AccountActivity directly, specifying the upper bound of the type parameter to constrain the Activity}}inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any? >) =
        AnkoInternals.internalStartActivity(this, T::class.java.params)
Copy the code

Compile critical code

// Function definition decompilation
 private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      Intrinsics.reifiedOperationMarker(4."T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);// Note # 1: Due to generic erasings, the previously passed AccountActivity argument was replaced by its upper bound constraint Activity, so this proves our previous analysis.
   }
// Function call point decompilation
protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
      // Note point 2: The function call is not a simple function call. Instead, it replaces the activity. class at the definition with the call's explicit type argument accountActivity. class, and then generates new bytecode to insert into the call point.
}
Copy the code

Let’s dot the function a little bit to make the output clearer

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any? >) {
    println("call before")
    AnkoInternals.internalStartActivity(this, T::class.java.params)
    println("call after")}Copy the code

After decompiling

private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      String var3 = "call before";
      System.out.println(var3);
      Intrinsics.reifiedOperationMarker(4."T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);
      var3 = "call after";
      System.out.println(var3);
   }

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      String var4 = "call before";
      System.out.println(var4);
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);// Replace it with the exact type argument accountActivity.class
      var4 = "call after";
      System.out.println(var4);
   }
   
Copy the code

5. Restrictions on the use of parameter functions of the materialized type

There are two main limitations to use:

1, Java calls Kotlin in the implementation type parameter function limitations

Kotlin uses inline functions as an inline function, but Java can call inline functions without the inline function. Kotlin uses inline functions as an inline function. The loss of inline means that the implementation is not possible. To reiterate, Kotlin’s argument functions cannot be called in Java

2. Kotlin implements type parameter functions with limitations

  • You cannot call a function that takes an argument of an instantiated type with an uninstantiated type parameter as a type argument
  • An instantiated type parameter cannot be used to create an instance object of that type parameter
  • A companion object method that implements a type parameter cannot be called
  • The reified keyword can only be used to mark inline functions that implement type arguments, not classes and attributes.

Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~