With the introduction of generics, the concept of subtypes becomes more complicated. It is not easy to define methods with newly learned generics, but the compiler has various obstacles to use them. Listen as I break it down and put it back together.

subtypes

Any time you want to use A value of type A, you can replace it with A value of type B (as A value of A) and say that B is A subtype of A.

As you can see from the definition, any type is also a subtype of itself.

To put the definition more generally, “a small class can replace a large class.” Int is a subclass of Number because the set range of numbers represented by Int is a subset of the set represented by Number.

Is one class a subtype of another? This is an important problem for the compiler because it is checked every time you assign a value to a variable or pass an argument to a function. A variable is allowed to store a value only if the value’s type is a subtype of the variable.

Subtypes in generics

Before generics were introduced, subtypes were clearly defined, and it was straightforward to determine if one class was a subtype of another; for example, String is a subclass of CharSequence.

Once generics are introduced, it gets complicated, such as: Is List

a subclass of List

? In other words, can we use List

instead of List

? It’s kind of hard to make a quick judgment.

To solve this conundrum, two inferences need to be derived from the original definition:

  1. The range of parameters that a subtype method receives must not be smaller than that of a parent type method

Give a counter example: “1.0” Meituan claimed to accept WeChat and pay treasure to two kinds of payment, its subtypes “Meituan 2.0” claims can only accept WeChat payments, if use “Meituan 2.0” replacement “Meituan 1.0”, the original and the “1.0” Meituan interaction code may be an error, because they tend to pay treasure, but can’t deal with it “Meituan 2.0”. In order for “Meituan 2.0” to replace “1.0,” it must support at least wechat and Alipay, and there is no reason to add other payment methods.

  1. A subtype method must not return a greater range of values than a parent type method

A counterexample: The “Merchant 1.0” poster included rice and soup, so I prepared chopsticks and spoons. Its subgenre, “Merchant 2.0”, has suddenly added apples, but I’m not ready to eat them. So businesses can’t return more stuff, or I can’t cope.

For List

to subclass List

, the following two conditions must be met:

  1. List<Sring>The range of parameters received by the method inList<CharSequence>The method of
  2. List<Sring>The range of values returned by the method inList<CharSequence>The method of

List is defined as follows:

// List with generic definition
public interface List<E> extends Collection<E> {
    boolean add(E e);
    E get(int index);
}

// List
      
public interface List<String> extends Collection<String> {
    boolean add(String e);
    String get(int index);
}

// List
      
public interface List<CharSequence> extends Collection<String> {
    boolean add(CharSequence e);
    CharSequence get(int index);
}
Copy the code

Although String get(int index); Return value range than CharSequence get(int index); Small. Satisfies the second condition.

But the Boolean add (String) e; Boolean add(CharSequence e); Small. It doesn’t satisfy the first condition.

So List

is not a subtype of List

. In other words, it is not safe to replace the List

with the List

in the program, because there may be code such as:

val spannable = Spannable()
val list: List<CharSequence> = mutableListOf()
list.add(spannable)
Copy the code

If you replace List

here with List

, the compiler will report an error. Because Boolean add(String e); Only String arguments will be handled; Spannable is out of scope.

Not to distort

To express the above example as a more abstract definition:

Suppose the generic class A contains the type parameter T, that is, class A

, and Type1 is A subclass of Type2. If there is no parent-child relationship between A

and A

, then class A is said to be invariant on the type parameters.

Classes in Kotlin and Java are immutable.

This is a bit of a constraint, and we’ve worked hard to abstract a method that takes a List

as an argument: handle(chars: List

), assuming the compiler will report an error if we pass the List

in… Do you need to redefine the same method for List

?

covariance

Invariance describes the fact that there is no subtype relationship between generic classes. There is also a subtype relationship between generic classes called covariant.

Covariant means that the class changes in the same direction as the abstraction of its type parameters.

(When trying to sum up an abstract concept, he often says something that makes people confused…)

In other words: as the type parameters become more concrete, the class also becomes more concrete. As the type parameters become more abstract, the class becomes more abstract.

For example, if you go from List

to List

, the type parameter changes from String to the more abstract CharSequence, and if the direction of change from List

to List

is also more abstract (the former is a subclass of the latter), The List

is said to be covariant on the type parameter T. (Obviously this example is not covariant but invariable)

If a generic class is covariant, it means that it preserves subtype relationships for type parameters at the class level

In Kotlin, to declare that the class is covariant on the type parameter, we need to add the out reserved word:

class MyList<out T>{... }Copy the code

While declaring a generic class as covariant makes the subtyped relationship more intuitive, it comes at a cost:

class MyList<out T> {
    fun set(item: T) {}// 错 错: Type parameter is declare as "out" but occur at "in" position in Type T
    fun get(a): T {...}
}
Copy the code
  • If T appears in the parameter bit of the method, the set(item: T) is said to consume a value of type T.

  • If T occurs in the return value bit, it is called GET (): T produces a value of type T.

When T is decorated by out, it can only appear in the return value bit, that is, it can only be produced by the generic class, not consumed.

So out has two effects:

  1. It preserves the subtyping of generic classes.
  2. It limits the type parameter to only appear in the return value bit.

The two points are complementary: because it restricts type parameters from appearing in parameter bits, subtyping is preserved. Because it preserves subtyping, type parameters can only appear in the return value bit.

If the type parameter appears in the parameter bit, it will appear in the following case:

class MyList<String> {
    fun set(itme: String)
    fun get(a): String
}

class MyList<CharSequence> {
    fun set(itme: CharSequence)
    fun get(a): CharSequence
}
Copy the code

Because fun Set (itme: String) can accept a smaller range of parameters than fun Set (itme: CharSequence), which does not conform to the first fallback, MyList

is not a subtype of MyList

.

Adding out, however, tells the compiler to remove the method that failed to preserve subtyping:

class MyList<out T> {
    fun get(a): T {...}
}

class MyList<String> {
    fun get(a): String
}

class MyList<CharSequence> {
    fun get(a): CharSequence
}
Copy the code

Fun GET (): String returns a smaller range than fun Get (): CharSequence, which fits the second corollary, so MyList

is a subtype of MyList

.

inverter

In addition to invariant and covariant, there is another subtype relationship between generic classes: contravariant.

Contravariant means that a class changes in the opposite direction to the abstraction of its type parameters.

In other words: As the type parameters become more concrete, the class becomes more abstract. As type parameters become more abstract, classes become more concrete.

Contravariant is a bit counterintuitive, it wants to achieve the effect that List

becomes a subtype of List

.

If a generic class is contravariant, it means that it reverses the subtype relationships of type parameters at the class level

In Kotlin, to declare that a class is invert on a type parameter, add the in reserved word:

class MyList<in T>{... }Copy the code

Again, there is a price to pay:

class MyList<in T> {
    fun set(item: T) {}
    fun get(a): T {... }// 错 错: Type parameter is declare as "in" but occur at "out" position in Type T
}
Copy the code

When T is modified by in, it can only appear in the argument bit, i.e. it can only be consumed by the generic class, not produced.

It follows that:

Out and int not only limit where arguments can appear, but also what classes can be subtypes.

Types of projection

The projection in our life is to take a three-dimensional object and turn it into a two-dimensional object, and the projection looks like it’s the same object just one dimension down.

A type projection in a program has a similar meaning:

Projecting a type means that some capabilities of the type are retained and others are removed. Type projection allows you to dynamically change the subtype relationships of generic classes.

Type projection is commonly used to dynamically transform invariant generic classes into inverters or covariant classes.

For example, a MutableList is a static one:

public interface MutableList<E> : List<E>, MutableCollection<E> {
	// The type argument appears in the out position
    public fun removeAt(index: Int): E
	// The type argument appears at the in position
    public fun add(index: Int, element: E): Unit. }Copy the code

MutableList

is invariant, so generic parameters can appear in either the in or out positions at will.

But invariance can sometimes narrow the scope of the method, such as:

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

This is a way to copy a set. Generics were introduced to avoid having to redefine the method for each specific type. Now this method can copy contents in any two lists of the same data type.

But what if I want to copy a collection of strings into a collection that can contain any object?

val strings = mutableListOf( "a"."b"."c" )
val anys = mutableListOf<Any>()

copy( strings, anys )/ / an error
Copy the code

Because the definition of copy() requires that the source and destination collections have the same type.

To make the copy() method work in this case, rewrite it like this:

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

Introduce a second generic type R, which is a subtype of T, and specify it as the source collection type parameter.

This change suddenly expands the range of argument types that the source parameter can accept. Instead of using only the same type as destination, it can now use all subtypes of destination.

This change can be simplified by using variations:

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

Where the source parameter is declared, an out type projection occurs, which removes all methods with consumption type parameters from the MutableList and retains all methods with production type parameters.

Although the source argument has lost some of its power, the sacrifice is always rewarded, and the range of types that source can accept has been expanded. (As it happens, the body of the copy() method doesn’t need the capabilities that source loses either.)

MutableList

and MutableList

public interface MutableList<out T> {
	// Methods whose type arguments appear in the out position remain unchanged
    public fun removeAt(index: Int): T
	// Methods whose type arguments appear in the in position are overwritten
    public fun add(index: Int, element: Nothing): Unit. }Copy the code

The out reserved word commands the compiler to overwrite all the methods in the generic class that consume type arguments, changing the argument type in the in position to Nothing.

Nothing is a subclass of all classes, so why do I change that? Because you want MutableList

to be a subtype of MutableList

.

Recall two corollary to the sudden-type:

  1. The range of parameters received by a subtype method must not be smaller than that of a parent type method
  2. A subtype method must not return a greater range of results than a parent type method

When there is no out projection, public fun add(index: Int, element: String): Unit accepts arguments with a range smaller than public fun add(index: Int, element: CharSequence): Unit accepts a range of parameters, which does not conform to rule 1, so MutableList

is not a subtype of Group

.

Change it to Public Fun Add (index: Int, Element: Nothing): Unit. Nothing is a subclass of all classes, it cannot be instantiated, and has no subtypes. In other words, if a method accepts an argument of type Nothing, it means that Nothing can be passed as an argument (the only Nothing that can be passed cannot be instantiated). This way public fun add(index: Int, element: String): Unit can take arguments that are larger than “nothing” (at least it can take strings).

Similarly, the in reserved word commands the compiler to override all production-type arguments in a generic class, changing the out position argument to Any? .

public interface MutableList<in T> {
	// Methods whose type arguments appear in the out position are overwritten
    public fun removeAt(index: Int): Any?
	// Methods whose type arguments appear in the in position are reserved
    public fun add(index: Int, element: T): Unit. }Copy the code

Any? Is the parent of all classes, which neatly makes MutableList

fit the second corollary (Any? Is already the maximum range. Whatever type is returned is a subclass of it.)

Finally, there is a special projection called the Star projection, which has the effect of adding the in projection and the out projection. (See table below)

There are three types of projections in Kotlin, summarized as follows (where Group, Dog, and Animal are all class names, and Dog is a subtype of Animal) :

Projection type Projection instance variant Inheritance relationships limit
Out of projection Group< out Animal > covariance Group< Dog > is a subclass of Group< out Animal > Type parameters cannot be used as method parameters
In the projection Group< in Animal > inverter Group< in Animal > is a subclass of Group< Dog > Type parameters cannot be used as method return values
Star projection Group< * > Group< any type > is a subclass of Group< * > Type parameters cannot do method parameters and they cannot do return values

Recommended reading

  • Kotlin base | entrusted and its application
  • Kotlin basic grammar | refused to noise
  • Kotlin advanced | not variant, covariant and inverter
  • Kotlin combat | after a year, with Kotlin refactoring a custom controls
  • Kotlin combat | kill shape with syntactic sugar XML file
  • Kotlin base | literal-minded Kotlin set operations
  • Kotlin source | magic weapon to reduce the complexity of code
  • Why Kotlin coroutines | CoroutineContext designed indexed set? (a)
  • Kotlin advanced | the use of asynchronous data stream Flow scenarios