preface

In Java, there is the concept of generics, but there has not been a unified comb, there are many knowledge points also fill in its details, so this chapter is ready to comb.

To get a better understanding, I’m going to go straight through the simplest generics use, generic erasing, Kotlin’s implementation of type parameters, covariant, contravariant, point variant, and so on.

The body of the

Don’t say a word. Just turn it on.

Basic concepts of generics

We’re always generics, so what are generics?

A generic is a type that can be defined with type parameters, and when instances of that type are created, the type type is replaced with the concrete type of the type argument.

So the emphasis here is on type parameters, we are familiar with parameters, such as method parameters when the method is defined as parameters, call is the argument passed to the method, the type parameter is we usually see in the class or method T, the T is the type parameter.

// where E is the type parameter
public interface MutableList<E> : List<E>
Copy the code
// String is a type argument
val list: MutableList<String> = mutableListOf()
Copy the code

Here we pass arguments just like we do when we call methods, except that the arguments we pass are types.

Kotlin always requires type arguments to be specified

Since I need to pass an argument to create an instance, this type argument should be passed, but Java generics are not available until 1.5, so I can create a generic class instance without passing an argument.

// In this Java code, the tempList type is List
List tempList = new ArrayList();
Copy the code

However, this is not allowed in Kotlin, because Kotlin was created with the concept of generics in mind, either by showing the types of generic parameters or by automatic derivation.

// The type argument is already specified as String
val list: MutableList<String> = mutableListOf()
// Can automatically deduce that the type argument is String
val list1 = mutableListOf("a"."b")
Copy the code

But the following code does not compile through the IDE:

// We can't know the type parameter
val errorList = listOf()
Copy the code

For Kotlin, which must provide arguments to type parameters, this is also a great way to reduce common code errors.

Declare generic functions

So let’s go step by step and see how we declare generic functions, and in our collection class, there are a lot of generic functions, so let’s look at one.

// 
      
        defines a type parameter
      
// Both the receiver and the returnee use this parameter
public fun <T> List<T>.slice(indices: IntRange): List<T> {
    if (indices.isEmpty()) return listOf()
    return this.subList(indices.start, indices.endInclusive + 1).toList()
}
Copy the code

There is nothing to be said here except to define type parameters after the fun keyword and before the method name.

To use this function, pass a type argument to the mutableList() function:

// The type is automatically derived. The type argument to List is String
val list1 = mutableListOf("a"."b")
// Display passes a type argument to slice
val newList = list1.slice<String> (0.1)
Copy the code

So a generic function can be a function on an interface or a class, a top-level function, an extension function, whatever.

Declare generic properties

A generic property is a bit different from a generic function in that it can only be defined as an extended property, not as a non-extended property.

// Define an extension attribute called last
val <T> List<T>.last : T
    get() = this.last()
Copy the code

For example, the above code defines last as an extended attribute for List, but you can’t define non-extended attributes:

// Class defines this compiler however
val <E> e: E
Copy the code

The above code will definitely not compile because you cannot store multiple different types of values in a class property.

Note that you define a new type parameter. If you define a generic class, you define a type parameter, which of course can be an attribute of the class.

Declare generic classes

Declaring a generic class is also very simple. Just like an interface, place the type parameters you want to define after the class name by <>.

// Define the type parameter directly after the class name
public interface List<out E> : Collection<E> {
     // This parameter can be used inside a method
    public operator fun get(index: Int): E
Copy the code

There’s nothing to say here.

Type parameter constraint

Type parameter constraints can constrain the types of the type arguments of a generic class or method.

It’s also easy to understand how to constrain the scope of a type parameter, which is expressed using extends in Java and a colon in Kotlin. <T:Number> means that the type parameter must be Number or a subtype of Number.

// Define type parameter constraints with the upper bound Number
fun <T : Number> List<T>.sum(): Number{
    / /...
}
Copy the code
// It can compile
val list0 = arrayListOf(1.2.3.4.5.6.7.8)
list0.sum()
// Failed to compile
val list1 = arrayListOf("a"."b")
list1.sum()
Copy the code

Multiple upper bound constraints can also be added here. For example, if type T has two upper bounds A and B, the type arguments passed in must be subtypes of A and B.

Makes the type parameter non-empty

We said we can add constraints on type parameters, but if we don’t, then the default upper bound for type T is Any, right? Notice that this is nullable.

// There is no constraint, the upper bound of T defaults to Any?
class TestKT<T> {
    fun progress(t: T){
        // Can be nullt? .hashCode() } }Copy the code
// So we can pass null when calling
val stringNull = TestKT<String? >() stringNull.progress(null)
Copy the code

Since Java has no concept of nullable types, I want to make the type non-null by specifying an upper bound, Any.

// Where type parameters cannot be passed nullable type arguments
class TestKT<T : Any> {
    fun progress(t: T){
        t.hashCode()
    }
}
Copy the code

Type erasure

Java/Kotlin generics are pseudo-generics because the type parameter is erased at run time, largely because it takes less memory.

We need to understand the concept of basic types, such as List< Int >, whose basic type is List. When the code runs, it only knows that it is List. Here is a classic example.

// Define a List that holds an Int
val listInt = arrayListOf(1.2.3)
// Define a List that holds a String
val listString = arrayListOf("hello"."java")
// At runtime, their classes are the same, both are lists
if (listInt.javaClass == listString.javaClass){
    Logger.d("Same type")}// Reflection allows you to add a String to a List of ints
listInt.javaClass.getMethod("add".Object: :class.java).invoke(listInt,"aa")
// Select * from ()
Logger.d("listInt size = ${listInt.size}")
Copy the code

List< Int > and List< String > are both lists at run time. We don’t know what type of List it should hold.

Type checking

With the type erasure feature, generic classes have some constraints on their types, such as type checking.

Type checking in Kotlin is done through the is function, but when type arguments are erased, you cannot directly use the following code:

// Define an ArrayList
      
val listInt = arrayListOf(1.2.3)
ArrayList
      
        this code does not compile
      
if (listInt is ArrayList<String>){
    
}
Copy the code

The above code will not compile and will report an error because it does not carry any type arguments at runtime. Here you can only tell if it is a List.

val listInt = arrayListOf(1.2.3)
// We can determine if this variable is a HashSet
if (listInt is HashSet<*>){

}
Copy the code

In addition to the is function being restricted, there is also the AS function.

Type conversion

The as function is also affected by type erasure.

/ / receive the Collection
fun printSum(c: Collection<*>){
    // An exception is actively thrown when the conversion fails
    val intList = c as? List<Int> ? :throw IllegalArgumentException("The expectation is a list")
    Logger.d("${intList.sum()}")}Copy the code

After defining the function, let’s call:

val intSet = hashSetOf(1.2.3)
printSum(intSet)
Copy the code

This code directly throws an exception indicating that the expectation is a List.

We’ll pass it a list, but not an Int:

val stringList = arrayListOf("a"."b")
printSum(stringList)
Copy the code

This line of code execution throws a different error:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

If you think about type erasure of generics, the first call is bound to fail, because even erasure of generics has the wrong base class type. The second call code can compile, but with a warning:

Because the compiler knows that the generic argument will be erased and that this operation is dangerous, it will still report a conversion error later.

The above two functions are also dangerous operations that the compiler will warn you of directly during your coding phase to prevent exceptions due to generic type erasure.

Declare a function that takes an argument of the implemented type

Unlike Java, Kotlin can implement type parameters, meaning that they are not erased at runtime.

For example, the following code will not compile:

fun <T> isAny(value:Any): Boolean{
        // Due to type erasure, the value of this T is not carried during execution
    return value is T
}
Copy the code

But Kotlin can make the above code compile and execute successfully, as follows:

// The function is declared inline and the generic parameter is modified with reified
inline fun <reified T> isAny(value:Any): Boolean{
    // The command is successfully executed
    return value is T
}
Copy the code

Can retain type above the key argument is not only the type argument using reified, must also be in inline function, because the compiler to implement inline function bytecode inserted into each call takes place, so each call to bring real type parameters of function, the compiler knows in the call as the type the exact type of arguments, The value of the type argument is replaced directly into the bytecode, so it is not erased.

As for normal inline functions, Java can call them, but not inline them. Java cannot call an inline function with a reified type argument because it must be inlined to implement the ified function and does some processing.

Implement type parameters instead of class references

Another scenario for implementing type parameters is when an API builder receives a java.lang.Class parameter. For a simple example, we know that startActivity requires an Intent that knows the Class type of the jump Class:

public Intent(Context packageContext, Class
        cls) {
    mComponent = new ComponentName(packageContext, cls);
}
Copy the code

If I can implement a Class directly, I can rewrite it:

// Direct type implementation
inline fun <reified T: Activity> Context.StartActivity(){
    // When a type is instantiated, T can represent a specific type
    val intent = Intent(this.T: :class.java)
    startActivity(intent)
}
Copy the code

It is also easier to call:

StartActivity<AboutUsActivity>()
Copy the code

Constraints on type parameters

Since Kotlin’s implementation-type argument is useful, but it has many limitations, let’s briefly summarize what we can do:

  • Is and AS are used in type checking and conversion

  • Use the Kotlin reflection API

  • Get the corresponding java.lang.class

– as a type argument to call another function

The following operations cannot be performed:

  • Cannot create an instance of a class specified as a type parameter

  • Calls the method of the companion object of the type parameter class

  • Used in non-inline functions

variant

Moving on to generics, we’ve introduced a concept called variations, what are variations, and it’s important to understand that, first of all, it’s literally a variation/variation generic parameter that describes how types that have the same base type and different types of arguments are related.

The List< String > argument is a subtype of Any. The String argument is a subtype of Any. The String argument is a subtype of Any. That’s what the variant is used to describe.

Why are there variations

Now that we know the concept of variants, where does this apply? When passing an argument to a parameter, we can look at an example:

// Function arguments are expected to be of type List
      
fun printContent(list: List<Any>){
    Logger.d(list.joinToString())
}
Copy the code

After defining the above function, I pass in a String set directly and what happens:

// It works fine here
val strings = arrayListOf("a"."bc")
printContent(strings)
Copy the code

Here we know that String is a subtype of Any, so we expect List< Any >, but we pass in List< String >.

// Pass in a mutable list, then add an element
fun addContent(list: MutableList<Any>){
    list.add(42)}Copy the code

After defining the above function, pass the string set and:

There is a difference between a List< T > and a MutableList< String >. There is a difference between a List< T > and a List< String >.

We pass a list of String arguments to a list of expected Any arguments, but it is not safe to add or replace values in a function. It is safe to simply fetch values from a list. Of course, the above conclusion is that we are familiar with lists and the MutableList interface, so we need to generalize this problem to any generic class, not just lists, which is why we need to have variants.

Class type subtype

So before we go on to variants, let’s just go over these three things.

In common use, we always think of classes as types, which is certainly not true.

For a non-generic class, let’s say Int is a class, but there are two types, Int and Int, right? Type, where Int? A type can hold not only Int data but also null.

For generic classes, a single class corresponds to an infinite number of types. For example, List is a class, which has List< Int >, List< String >, and so on.

The subtype definition is very important. The specific definition is that whenever A value of type A is needed, the value of type B can be used as the value of type A, so that type B is A subtype of type A.

For example, if I define a variable a of type Any, and I use a value of type String as a value of type A, which of course is ok, then the question is, String is a subclass of Any, is that the same relationship

Subclasses and subtypes are pretty much the same thing for non-generic classes, but Kotlin does have nullable types, like Int is Int? Type, but they are the same class. So for a generic class, we saw earlier that when I need List< Any > we can pass it List< String >, so List< String > is a subtype of List< Any >, If there is a MutableList< Any >, we cannot use a MutableList< String >.

To summarize the terminology: for A class like List, where A is A subtype of B and List< A > is A subtype of List< B >, the class is said to be covariant; For A class like MutableList, A MutableList< A > is said to be invariant in terms of the parameters of any two types A and B, either A subtype of the MutableList< B > is not A supertype.

Covariant: Preserve the subtyping relationship

To understand covariant variants of generic parameters, it is necessary to understand the concept of subtyping mentioned above so that you can understand why they are placed in and out.

If I have a generic class called List< T >, and String is a subtype of Any, then List< String > is a subtype of List< Any >, and the class is covariant on T, so I need to modify T with out.

The most common covariant generic class is the List in Kotlin, which looks like this:

public interface List<out E> : Collection<E> {
   
    override val size: Int
    override fun isEmpty(): Boolean
    }
Copy the code

List is also one of the best covariant examples. Let’s try it out:

// Define the animal class
open class Animal{
    fun feed(){
        Logger.d("zyh animal feed")}}Copy the code
// Define a Herd, assuming that a Herd contains 10 animals
class Herd<T : Animal>{
    // The upper bound is Animal, so the lower implementation saves the Animal type
    val animal = Animal()

    fun getVa():T{
        return animal as T
    }
    // There are ten animals in a herd
    val size = 10
}
Copy the code
// Feed the herd
fun feedAll(animals: Herd<Animal>){
    for (i in 0 until animals.size){
        // Walk through and take out Animal, then throw food
        animals.getVa().feed()
    }
}
Copy the code
// Define the cat to the animal
class Cat : Animal(a){
    // Need to clean up
    fun clean(){
        Logger.d("zyh cat clean")}}Copy the code

I have a herd of 10 animals and want to feed them, and now I want to feed the cats:

// The method parameter is a flock of cats
fun feedCatHerd(cats: Herd<Cat>){
    for (i in 0 until cats.size){
        cats.getVa().clean()
    }
    // The cats belong to the herd, so I feed them all at once
    feedAll(cats)
}
Copy the code

This seems reasonable, but the code doesn’t compile:

There’s a mismatch in the hint type, so there’s a problem here, so let’s sort it out a little bit.

Herd< Cat > = Animal< Animal > = Herd< Cat > = Animal< Animal > = Herd< Cat > = Animal > So I can deal with the above problem, so this variant relationship is called covariant, just add out to the generic parameter of Herd to show that the Herd is covariant in that parameter.

The type parameter T is covariant
class Herd<out T : Animal>{

    val animal = Animal()

    fun getVa():T{
        return animal as T
    }

    val size = 10
}
Copy the code

In position and out position

Can we make all type parameters out? Of course not, type parameters defined as out can only be used in generic classes in the out position, which is the position of the return value, or the producer position, That is, generate a value of type T.

// For example, here
interface Transformer<T>{
    // The position of the method argument is in position
    fun transform(t: T): T
    // The return position of the method is the out position
}
Copy the code

So adding the out keyword to the type parameter T has two meanings:

  • Subtyping is preserved

  • T can only be used in the out position

So when the type parameter is defined as out, the type parameter cannot appear in the in position in the generic class, and the compiler will report an error.

Inverse: inverse rotor typing relationship

Now that we know the concept of covariant, it’s easy to understand contravariant, which is the annoying typing of covariant.

Direct definition: A class that inverts type parameters is A generic class (let’s take Consumer< T > for example) for which Consumer< A > is A subtype of Consumer< B > if B is A subtype of A.

For contravariant, let’s also take an example:

// This is the sorting function for iterating sets and. We pass in a Comparator whose type is in T
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
    if (this is Collection) {
       if (size <= 1) return this.toList()
       @Suppress("UNCHECKED_CAST")
       return(toTypedArray<Any? > ()as Array<T>).apply { sortWith(comparator) }.asList()
    }
    return toMutableList().apply { sortWith(comparator) }
}
Copy the code

Then we have the following code:

// The generic argument is Any
val anyComparator = Comparator<Any> { o1, o2 -> o1.hashCode() - o2.hashCode() }
// The generic argument is Number
val numberComparator = Comparator<Number> { o1, o2 -> o1.toInt() - o2.toInt() }
// The generic argument is Int
val intComparator = Comparator<Int> { o1, o2 -> o1.toInt() - o2.toInt() }

// Create a set of type Number and, according to the interface, expect a Comparator
      
val numbers: List<Number> = arrayListOf(1.11.111)
// It can compile
numbers.sortedWith(numberComparator)
// It can compile
numbers.sortedWith(anyComparator)
// Cannot compile
numbers.sortedWith(intComparator)
Copy the code

< Int > < Number > < Any > < Number > < Any > < Number > < Any > < Number > < Any

Because the contravariant type parameter marked in needs to be entered, that is, it needs to use an instance of the class. For example, the expectation here is Number, I call the method in Number, but I get a subtype Int, if Int has a getInt method, then the type parameter in the generic class is Number, It doesn’t have a getInt method, so it’s not allowed. But there’s a supertype of Any, and I can do anything in Any with Number, so it’s safe.

Here, we put covariant and contravariant all said, in fact, do not have to memorize, to understand the ideas and principles of library design, this is easy to understand.

Point variant: Specifies the variant where the type occurs

As we said at the beginning, variants describe the relationships between generic class types, which can be classified as covariant, contravariant, or invariant depending on whether subtyping is preserved. In addition to this, we also need to grasp the concept of point variants.

In fact, the above mentioned covariant and contravariant classes are used when defining generic classes or functions, which is still not very convenient. For example, the generic parameters of the MutableList class are invariant, but there are some requirements that we want to change the definition, so let’s take a look at the example.

We want to write a function that copies MutableList as follows:

// The arguments are mutableList
fun <T> copyData(source: MutableList
       
         , target: MutableList
        
       ){
    for (item in source) {
        target.add(item)
    }
}
Copy the code

Then when we use:

val strings = arrayListOf("zyh"."wy")
var targets: ArrayList<String> = ArrayList()
// Both types are strings, of course
copyData(strings,targets)
Copy the code

I could define a String sum, copy it to Any, and the logic would be logical:

MutableList< String > is a MutableList subtype that is not Mutable< Any >. If a member of a Mutable class is a member of a Mutable subtype, then a member of a Mutable subtype is a Mutable subtype. If a member of a Mutable subtype is a Mutable subtype, then a member of a Mutable subtype is Mutable.

// Define two type parameters directly to restrict the relationship between the two classes
fun <T : R,R> copyData1(source: MutableList
       
         , target: MutableList
        
       ){
    for (item in source) {
        target.add(item)
    }
}
Copy the code

Then when we use:

val strings = arrayListOf("zyh"."wy")
var targets: ArrayList<Any> = ArrayList()
copyData1(strings,targets)
Copy the code

There’s no problem with that, but it’s too much trouble, Kotlin and Java both have a more elegant way of dealing with it, which is point variants, what is point variants like in this case I think souce is covariant, which is very common in Java, because all generics in Java are defined immutable, Only if the variant is specified when in use, let’s look at the Java code:

// use 
       indicates that a subtype of T can be used
public static <T> void copyDataJava(ArrayList<? extends T> source
        , ArrayList<T> destination){
    for (int i = 0; i < source.size(); i++) {
        destination.add(source.get(i));
    }
}

private void  testJava(){
    ArrayList<String> strings = new ArrayList<>();
    strings.add("zyh");
    strings.add("wy");
    // The type is Object, but no error is reported
    ArrayList<Object> objects = new ArrayList<>();
    copyDataJava(strings,objects);
}
Copy the code

This code is much simpler if Kotlin is used:

// Add an out modifier
fun <T> copyData2(source: MutableList
       
         , target: MutableList
        
       ){
    for (item in source) {
        target.add(item)
    }
}
Copy the code

The type class that specifies the source directly is covariant, so that its subtypes can be passed.

Point variants are important, not only for method parameters, but also for local variables, return types, and so on.

projection

If there is a MutableList< T >, then there is a MutableList< T > in T. If there is a MutableList< T > in T, then there is a MutableList< T > in T.

For example:

fun <T> copyData2(source: MutableList
       
         , target: MutableList
        
       ){
    for (item in source) {
        target.add(item)
    }
}
Copy the code

So the source here is actually not a regular MutableList, it’s a projection of what it is now, and this is called a type projection, meaning it’s not really a type, it’s just a projection.

In fact, it makes a lot of sense to think about a projection, because it’s a restricted type, not really a type, because some of its functionality is restricted, it’s a fake type.

For example, in the code above, marked for covariant, the method used by the MutableList in position will not be called,

The add method in the MutableList is the position of the generic parameter in, so it cannot be used directly here, which is limited in function, and also proves why it is called a projection.

And this is actually pretty straightforward, because once you covariant a type with a point variant, you can’t use the method of type parameters in position in, and then this new “false” type is called a projection.

The asterisk projection

Projection is a restricted type, such as a type parameter that can be used in and out. When a point is deformed to out, the generated projection type will not be able to use the method of the argument in position.

An asterisk projection is also a projection with the limitation that it is equivalent to out, that is, only methods that use type parameters in the out position.

After we understand the definition directly, we need to think about why we have asterisk projections. We can use asterisk projections when type parameters are not important. What is unimportant when we don’t call a method with type parameters or just read data without caring about its type.

For example, when I want to determine if a set sum is an ArrayList:

val strings = arrayListOf("zyh"."wy")
// Only asterisks can be used here
if (strings is ArrayList<*>){
    Logger.d("ArrayList")}Copy the code

Because generics are erased at run time, type parameters are not important, so asterisk projections are used here.

Asterisk projection and Any?

Asterisk projections are easy to misunderstand, such as MutableList< * > and MutableList< Any? < span style = “max-width: 100%; clear: both; min-height: 1em; > this list can contain any type, but MutableList< * > is a list of a type that you don’t know about until it’s assigned.

Let’s look at an example:

We don’t know the type of Unknowns before the assignment, but after the assignment, we can get the value of Unknowns through GET, but we can’t call the ADD method. This limitation is the covariant of out point variant.

So here the asterisk projection is converted to < out Any? >, can only call a method whose type argument is in position out, cannot call a method whose type argument is in position.

conclusion

After almost a day of writing, I finally combed through all the Kotlin generics and felt much happier in my heart.