Kotlin’s generics foundation is very similar to Java, so I recommend that you learn about Java generics before you learn about Kotlin’s generics, or at least understand the wildcard,
and
, how to write generic functions, generic classes, what is the nature of generics?

The generic

For me, I came over from c++, where there are no generics, only templates, and c++ generates code from templates based on the types encountered during compilation, one piece of code for each type of template (I don’t know if it’s still implemented that way, I only learned c++11), But I learned that generics are really just like function arguments: functions pass parameters inside (), whereas generics <> pass types. They’re basically arguments, but one is a variable and the other is a type

Generics for functions, parameters, attributes, and classes

fun <T> print(t: T) {
   println(t)
}

class GenericsDemo01<T>(val f: T) {
   fun print(t: T) {
      println(t)
   }
}
Copy the code

Generic constraint

Many times we need to constrain the type of generics to a certain bound, such as the generics of the sum function

fun <T> sum(a: T, b: T): T {
    return a + b // error
}
Copy the code

Arguments A and B do not support the + operation for all types, so we need to restrict the type arguments (generics) passed in, like the following

fun <T : Integer> sum(a: T, b: T): T {
    return a + b
}
Copy the code

This is similar to Java’s

, which qualifies that T must inherit from Integer(or that T must be a subclass of Integer).

The Java<T extends Integer>For set generics, generic constraints are usually used for non-set generics, because set generics already have covariant and contravariant constraints and do not need the generic constraints of this chapter

That’s right, so you don’t have to do the awkward thing of passing in two a and B addition operators of type Any

The generic constraint does not control as strictly as the set generic constraint that T must be the same, you can use it like this:

private fun <T : Number> printT(a: T, b: T) {
   // a = 9999, b = 100.5
   println("a = $a, b = $b")
   // aClass = class java.lang.Integer, bClass = class java.lang.Double
   println("aClass = ${a.javaClass}, bClass = ${b.javaClass}")}fun main(a) {
   printT(9999.100.5)}Copy the code

A: T, b: T are two different types, one is Integer, the other is Double

The above code is similar to this code in Java

static <T extends Number> void printT(T a, T b) {
   System.out.println("a = " + a);
   System.out.println("b = " + b);
   System.out.println(a.getClass());
   System.out.println(b.getClass());
}

public static void main(String[] args) throws Exception {
   printT(10.20.1);
}
Copy the code

I don’t know how to write sum in generics, so I think I can only use reflection to write sum

fun <T> sum(a: T, b: T): T {
   val clazz = a.javaClass
   val sum = clazz.declaredMethods.firstOrNull { it.name == "sum" } ?: throw Exception("can't find function. T not a subclass of Number")
   return sum.invoke(a, a, b) as T
}
Copy the code

Int sum(int, int); int sum(int, int) T of TYPE T is considered to be an Integer(the generic TYPE can only be an Integer), while sum requires an int. Integer.TYPE is an unwrapped int

Add multiple constraints to a generic

Where is similar to where in SQL statements

fun <T> ensureTrailingPeriod(seq: T): T where T : CharSequence, T : Appendable {
   if(! seq.endsWith(".")) {
      seq.append(".")}return seq
}
Copy the code

The benefits of constraints go beyond knowing that the classes we need must be subclasses of constraints and constraints. It also gives our T a lot of functions that constrain the class (including extension functions, etc.)

The endsWith function is an extension of CharSequence and append is a function of the Appendable interface

So does the sum function

Multiconstrained generics are also allowed in Java

static <T extends CharSequence & Appendable> T ensureTrailingPeriod(T seq)
Copy the code

A generic type can be NULL or non-null

The type T of a generic type is similar to a platform type. It’s up to the programmer to decide whether it’s null or not. Kotlin no longer manages nullability

fun <T> print(t: T){ t? .let { println(it) } }Copy the code

Generic runtimeType erasureandSolid type

As with Java, generics are erased at run time, exist at compile time, and are treated as Any at run time

fun <T> isIntList(list: List<T>) {
   if (list is List<Int>) { // There is an error
      println("This is wrong.")}}fun main(a) {
   val list = listOf(1.2.3)
   isIntList(list)
}
Copy the code

But here’s how:

if (list is List<*>)
Copy the code

The Kotlin compiler can determine generic types in the same scope

val list = listOf(1.2.3)
if (list is List<Int>) {
    println("That's okay.")}Copy the code

The following situation can also be problematic

Implements type parametersreified T

At runtime, the type is treated as Any, but it obviously cannot be cast to T. In this case, consider inline and reified

inline fun <reified T> isA(value: Any) = value is T

fun main(a) {
   val a: Int = 10
   println(isA<Int>(a))
}
Copy the code

As we learned earlier, inline lines copy code to all calls, so the Kotlin compiler can recognize the type of the generic at runtime

Inline was used in the previous section to improve performance and eliminate the side effects of lambda arguments on objects. In this section, inline is used to implement types. This is the second use scenario for inline

Another use of instantiated parameters is to get a Class object from a type passed as a parameter

inline fun <reified T> loadService(a): ServiceLoader<T>? {
   return ServiceLoader.load(T::class.java)
}

fun main(a) {
   val loadService = loadService<Int>()
}
Copy the code

Variants: Generics and subtyping

Classes, types, and subtypes

The difference between classes and types

In many cases, classes can be thought of as types in general, but in fact, classes and types are not the same thing: empty and non-empty types, Int and Int? , please confirm Int? Is the class? Isn’t that Int? Is it a type? Obviously, type

List

List

List

etc. These are types, and List is a class


Subtype relation

Subtypes are relationships that exist in Java classes, Java arrays, but are missing in Java generics

Whenever you need A value of type A, you can use A value of type B, which is called A subtype of type A.

To make things simpler, A pointer to class A (called A reference in Java) refers to an object of class B, so we can say that A’s subtype is B, and that’s it. (Val A: A = B())

For example, we have a reference to val a: Number and an object 10 of type Int. If the reference refers directly to the object val a: Number = 10 then we can say that Int is a subtype of Number and that Number is a supertype of Int

Simple: The supertype of a subclass is a superclass, and the subtype of a superclass is a subclass. Just remember this relationship

Covariant and contravariant

There will always be implementations of high-end concepts, and the level to which we learn is to describe these concepts in the simplest sentence

Covariance (covariant)

  1. What is?

Covariant: is a relation, a constant relation in which a parent class reference refers to a subclass object

  • A Number reference can always refer to an Int, so if Number’s subtype is Int, Number and Int are covariant

  • If the same reference to Number[] can always refer to Int[], Number[] and Int[] are covariant

  • Similarly, a reference to List

    can always refer to List

    , so we can also say that List

    and List

    are covariant



However, Java has historically used type erasers, so there is no contravariant relationship when any type is generic, because at runtime Java always changes the type to Listor just List. Some security issues arise if covariant relationships are forced out

If Java generics support for covariant existsThe problem

If generics support covariance, this can lead to the following problems:

Integer[] a = new Integer[2];
a[0] = 1000;
Object[] o = a;
o[1] = 'a'; / / complains here Java. Lang. ArrayStoreException: Java. Lang. Character
Copy the code

An Integer refers to a Character object that does not have a subtype relationship. An Integer refers to a Character object that has no subtype relationship

Replacing the above array entirely with a collection would produce the following code: (The following code will return an error under normal conditions, but will not return an error under full selection and type erasers)

List<Integer> list = new ArrayList();
list.add(1000);
List<Object> objList = list; // The parent class refers to the subclass object, which is technically correct. Object --> Integer
objeList.add(10.9); // This will compile and pass at run time, because the parent class still refers to the object that points to the subclass, object -> double
Copy the code

In the above collection, if the collection is full of both variants and type erasures, it will compile and run without error, because the type class is completely erased as Object at run time

This is not allowed, so it does not support covariation

To address the above problem, Java introduced covariants belonging to Java generics

Java generics are a solution to “vanishing covariant relationships”

Covariant relation is also called subtype relation

Java introduced wildcards? And then use List
means covariant and is equivalent to List

without type erasure. It accepts Number and subclasses of Number into the List

collection

So the List
Collections can be stored

Objects of these classes above

So how did he solve the above problem?

A: The Java solution is simple, one size fits all, if the type is
covariant, then it does not allow write, modify, etc. Read only

If I can’t solve the problem, I have to solve the problem, right??

List
it’s not easy to remember what kind of class you can store in it, you can just think of it as a List

that supports covariation, and the Number set can store it and its subclasses, and that’s called an upper bound because it limits the Number of classes, right

Covariant relation corresponding to Kotlin

In Kotlin, the covariance would be: 1. Covariance of type parameters at the class 2. Sets the covariant of generics at functions

Covariant of type parameters at class
interface Producer<out T> {
    fun produce(a) : T
}
Copy the code

Out is placed there and has two main functions:

  • Subtypes will be preserved (Producer<Cat>isProducer<Animal>A subclass of)
  • TCan only be used inoutlocation

The in position is in the function argument, and the out position is in the function return value. If you are both in and out, you do not need the flag. The generics of the out flag can only be read, but cannot be written, and the generics of the in flag can only be written but cannot be read.

  1. The arguments to the constructor use T as the generic type and are neither in nor out, but if you prefix the argument with val/var then you need to pay attention to in/out because it is no longer a parameter but a property, which has a setter/getter

  2. MutableList cannot use out

  3. The covariant set does not allow writing, only reading

  4. Covariant out B means only B or a subclass of B can be filled in

Covariant of set generics at function

Java-like usage

Out T corresponds to Java? extends T

In T corresponds to Java, right? super T

private fun f(list: ArrayList<out B>) {
   // Write is not allowed when used
// list.add(D()) // The argument becomes Nothing, so the D() object cannot be added
   list.forEach {
      println(it.javaClass)
   }
}
Copy the code

Contravariant: Inverse subclass relationship

Normally, Animal is the parent of Cat, Animal is Cat, List

is a subtype of List

, that’s covariant, but if List

is a subtype of List

, The seed type relationship is reversed, it’s contravariant



Kotlin’s contravariant

The same kotlin supports: 1. Generic parameter inversion of classes 2. Generic parameter inversion of function sets

Class to invert generic parameters
class A<in T> {
   fun write(t: T){}}Copy the code
Function set parameter generic contravariant
open class A
open class B : A(a)open class C : B(a)open class D : C(a)fun c(list: ArrayList<in C>) {
// list.add(A()) // ArrayList
      
// list.add(B()) // ArrayList
      
   list.add(C()) // Conforming superclass references refer to subclass objects
   list.add(D()) // Conforming superclass references refer to subclass objects
   list.forEach {
      println(it::class.java)
   }
}

fun main(a) {
   val l1: ArrayList<A> = arrayListOf(A(), B())
   val l2: ArrayList<D> = arrayListOf(D())
   Val l3: ArrayList
      
        = arrayListOf
       
        (D()
       
      
   val l4: ArrayList<in C> =
      arrayListOf(D()) // arrayListOf(D()) is an ArrayList
      
        type. The kotlin compiler made a nasty optimization to raise the type of the generic type from D to C, so it will not return an error
      
   c(l1) // The value must be of Any ~ C type
ArrayList
      
       (Any ~ c) {ArrayList
       
      
   c(arrayListOf(D())) // The type is ArrayList
      
       , so no error is reported
      
}
Copy the code
  1. When the contravariant is initialized (the argument is assigned to the parameter in C), it represents a range of types [Any, C] that do not refer to a specific type, but a broad range of types, which is the main function of the contravariant

  2. When used inversely, just think of it as an ArrayList

    . If I limit what I write to subclasses of C, I’ll limit the C function A little bit more, and there won’t be A lot of debate about why I don’t limit Any or A or B or C

To prevent the existence of qualified B, write as A, so that the assignment compatibility principle (parent class reference refers to the child object)

  1. The contravariant collection can only be written to, not read. When read, the type disappears and it returns[Any, C]Any of the possible types, so it’s returned directlyAnyThe type disappears

Why do I want to satisfy the assignment compatibility principle? I would like to say that contravariant is called inverse subtype relationship, but in fact, it is only a guarantee that a type is in a range, such as:

fun c(list: ArrayList<in C>) {
    list.add(C())
    list.add(D())
    list.forEach {
        println(it::class.java)
    }
}

fun main(a) {
    val l: ArrayList<A> = arrayListOf(A())
    c(l)
}
Copy the code

[Any, C] -> A [Any, C] -> A] [Any, C] -> A] [Any, C] -> A] A set of types -> A, C, Any -> A It still conforms to the assignment compatibility principle

Write it using covariant and contravariantcopyDatafunction

  1. This function is implemented in the normal way
fun <T> copyData01(source: MutableList<T>, destination: MutableList<T>) {
   for (item in source) {
      destination.add(item)
   }
}
Copy the code
  1. Implement this function using constraints
Source: MutableList
      
        source: MutableList
       
         MutableList
        
          is a list of subclasses * that give the item of source to destination */
        
       
      
fun <T : R, R> copyData02(source: MutableList<T>, destination: MutableList<R>) {
   for (item in source) {
      destination.add(item)
   }
}
Copy the code
  1. Implement functions in a covariant manner
/** * Use the out generic modifier * out T to denote T or a subclass of T */ for reading functions
fun <T> copyData03(source: MutableList<out T>, destination: MutableList<T>) {
   for (item in source) {
      destination.add(item)
   }
}

/** * in T: T's parent */
fun <T> copyData04(source: MutableList<T>, destination: MutableList<in T>) {
   for (item in source) {
      destination.add(item)
   }
}

/** ** the following is the declaration of the variant */
fun <T> copyData05(source: MutableList<out T>, destination: MutableList<in T>) {
   for (item in source) {
      destination.add(item)
   }
}

/** * Public interface List
      
        public interface List
       
         * /
       
      
fun <T> copyData06(source: List<T>, destination: MutableList<in T>) {
   for (item in source) {
      destination.add(item)
   }
}
Copy the code

In Kotlin, if a generic is marked out, it can only call functions that match the out position of the generic, such as fun get() : T. If a generic is marked in, it can only call functions that match the compound in position of that class, such as fun add(T: T): void

Out covariant, read only, in contravariant, can read and write, but write will lose type, no in/out invariant

A generic classoutinThe location of the

Kotlin supports defining generic variants in class declarations, as well as writing variants in function locations, as Java does

Asterisk projection: use*Substitute type parameter

  1. The asterisk projection does not know what type is stored, so it usually does not write, but only read

So functionally similar to List

, Any is the best choice without Any type of information

  1. Asterisks are used to indicate that the developer does not need to know exactly what type of generic is being read

Let’s just say, asterisk projection let’s call it out Any, right? Okay, so the object that’s read is Any, right? Object is ok, can’t write