preface

To better understand covariant and inverting in Kotlin and Java, let’s take a look at some basics.

Ordinary assignment

In Java, common assignment statements are as follows:

A a = b;
Copy the code

The assignment statement must satisfy the condition that the left is either the parent of the right or the same type as the right. For example, Object o = new String(“s”); . For convenience, it is called A > B below.

In addition to the most common assignment statement above, there are two other assignment statements:

Assignment of function arguments

public void fun(A a) {}
// Assign a value to the call
B b = new B();
fun(b);
Copy the code

When fun(b) is called, the argument b b passed in is assigned to the parameter A A, which is of the form A A = b. Similarly, the parameter type must be greater than the argument type, i.e. A > B.

Assignment of the returned value of a function

public A fun(a) {
    B b = new B();
    return b;
} 
Copy the code

The return value type of the function receives the value of the actual return type, which is B. B is assigned to the return value type A. B is assigned to the return value type A, which is B.

Therefore, any assignment must satisfy the left > right type, that is, A > B.

Covariant and inverter in Java

With the previous basic knowledge, it is easy to explain covariant and contravariant.

If class A > class B, trans(A) and trans(B) obtained after A change of trans still satisfy trans(A) > trans(B), then it is called covariant.

Contravariant is just the opposite. If class A > class B, trans(A) and trans(B) obtained after A change trans satisfy trans(B) > trans(A), it is called contravariant.

For example, we all know that Java arrays are covariant. If A > B, then A[] > B[] can be assigned to A[]. Here’s an example:

Integer[] nums = new Integer[]{};
Object[] o = nums; Object[] > Integer[]
Copy the code

However, Java generics are not subject to this change, as follows:

List<Integer> l = new ArrayList<>();
List<Object> o = l;// The compiler will not compile
Copy the code

List> List

= List

= List

= List

= List

= List

Naturally, you cannot assign List

to List
. Java generics do not support type variation.

How to implement covariant and inverse generics in Java

We know that generics do not support type-changing in Java, but this creates a strange confusion that has been mentioned in many generics articles:

If B is A subclass of A, then List<B> should be A subclass of List<A>. It’s a very natural idea!

Unfortunately, Java does not support it for a variety of reasons. However, Java does not completely eliminate the typecategy of generics. Java provides <? Extends T > and <? Super T> makes generics covariant and contravariant.

<? Extends T > and <? super T>

<? Extends T> is called an upper bound wildcard, <? Super T> is called the lower bound wildcard. Using an upper bound wildcard makes a generic covariant, while using a lower bound wildcard makes a generic contravariant.

Take the example I gave you earlier

List<Integer> l = new ArrayList<>();
List<Object> o = l;// The compiler will not compile
Copy the code

If I use an upper bound wildcard,

List<Integer> l = new ArrayList<>();
List<? extends Object> o = l;// Can be compiled
Copy the code

In this way, the List <? The extends Object> type is now greater than the List<Integer> type, and thus covariant. This is known as “a generic type of a subclass is a subclass of a generic type.”

Similarly, the lower bound wildcard <? Super T> can realize the inverter, such as:

public List<? super Integer> fun(){
    List<Object> l = new ArrayList<>();
    return l;
}
Copy the code

How does the above code achieve inverter? First, Object > Integer; In addition, we know from the introduction that the return type of the function must be greater than the actual return type, which in this case is List
> List, which is the opposite of Object> Integer. In other words, after the generic change, the type relationship between Object and Integer is reversed. This is invert. The invert is implemented by the lower bound wildcard
.

As can be seen from the above, <? The upper bound in extends T> is T, which means <? All types that extends T> refers to are subclasses of T or T itself, so T is greater than <? Extends T >. <? The lower bound in super T> is T, which means <? Super T> refers to types that are either superclasses of T or T itself, so <? Super T> greater than T.

Although Java uses wildcards to solve the problem of generics covariant and contravariant, many articles on generics are so difficult to understand that I once wondered what the hell is this? I didn’t know until I found an easy explanation on StackOverflow (yes, most of the previous part came from the god’s explanation on StackOverflow). And it makes sense when you get to the point that the left-hand side of an assignment statement has to be bigger than the right-hand side.

PECS

The PECS principle is that Producer Extends Consumer Super. Producers use the upper wildcard and consumers use the lower wildcard. This sentence can be confusing to read directly, so let’s go back to the source to see why it exists.

First, let’s write a simple generic class:

public class Container<T> {
    private T item;

    public void set(T t) { 
        item = t;
    }

    public T get(a) {
        returnitem; }}Copy the code

Then write the following code:

Container<Object> c = new Container<String>(); // (1) Compile error

Container<? extends Object> c = new Container<String>(); // (2) Compile passed
c.set("sss"); // (3) Compile error
Object o = c.get();// (4) Compile passed
Copy the code

Code (1), Containerc = new Container

(); Container

is not a subtype of Container
, so the value cannot be assigned.

Container<String> becomes Container<? Is a subtype of extends Object>, so it can be assigned.

If code (2) compiles, why does code (3) report errors? Because code (3) tries to assign a String to a <? Extends Object> Type. Obviously, the compiler only knows <? Extends Object> is some subtype of Obejct, but we don’t know which one, and it may not be a String, so we can’t assign a String to it directly.

As you can see from the above, for the use of <? The extends T> type cannot be written to elements, otherwise it will compile and report an error as in (3).

But you can read elements, such as code (4). And that type can only read elements. This is called a “producer.” That is, the person who can only read elements from it is the producer. Extends T> Wildcard.

Similarly for consumers, the code is as follows:

Container<String> c = new Container<Object>(); // (1) Compile error

Container<? super String> c = new Container<Object>(); // (2) Compile passed
 c.set("sss");// (3) Compile passed
 String s = c.get();// (4) Compile error

Copy the code

Code (1) compiles incorrectly because generics do not support inverters. And even if you don’t know generics, the code looks wrong at first glance.

Code (2) compiles because of the <? Super T> wildcard after generic inverting.

Code (3) compiles and assigns String to <? Super String >, <? Super String> refers to the superclass or String in general, so this is compilable.

Code (4) compiles an error because it tries to put the
is assigned to String, while
> String, so it cannot be assigned. In fact, the compiler has no idea what type to use to accept the return value of c.get(), because in the compiler’s eyes
is a generic type, and all the superclasses of String and the String itself are possible.

Also, as you can see from the above code, this is a good example for using
type is not allowed to read the element, otherwise the compiler will report an error as shown in (4). But you can write elements, such as code (3). This type can only write to elements. This is called a “consumer.” Super T> Wildcard.

In summary, this is the PECS principle.

Covariant and contravariant in Kotlin

Kotlin ditches wildcards in Java in favor of declarative variants and type projections.

The declaration is a variant

First, let’s go back to the definition of Container:

public class Container<T> {
    private T item;

    public void set(T t) { 
        item = t;
    }

    public T get(a) {
        returnitem; }}Copy the code

In some cases, we will only use Container
or Container
means that we only use Container as producer or Container as consumer.

So why do we need to define both get and set when defining Container? If you think about a class that only serves as a consumer, it would be unnecessary to define the get method.

Conversely, if a generic class has only producer methods, as in this example (from the kotlin official documentation) :

// Java
interface Source<T> {
  T nextT(); // Only the producer method
}
// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; / /!!!!!! Not allowed in Java. Use the upper bound wildcard 
      
  / /...
}
Copy the code

Storing a reference to a Source

instance in a variable of type Source
is extremely safe — there are no consumer-methods to call. However, Java still won’t let us assign values directly, so we need to use an upper bound wildcard.

But this makes no sense. Using wildcards only complicates the type and adds no additional value because the only methods that can be called are the producer methods. But the Java compiler is dead.

So, if we could determine whether a class was a producer or a consumer before we used it, wouldn’t it be nice to declare its role directly when defining the class?

So that’s Kotlin’s declarative typomorphism, defining the typomorphism behavior of the class directly at the time it’s declared.

Such as:

class Container<out T> { / / (1)
    private  var item: T? = null 
        
    fun get(a): T? = item
}

val c: Container<Any> = Container<String>()// (2) compile through, because T is an out- argument
Copy the code

(1) use

directly to specify that the T type can only appear at the producer’s location. The kotlin compiler can assign Container

to Container

as it did in (2), as if generics were covariant. Instead of using the Java wildcard
.

Similarly, for consumers,

class Container<in T> { / / (1)
    private  var item: T? = null 
     fun set(t: T) {
        item = t
    }
}

val c: Container<String> = Container<Any>() // (2) compile through, because T is an in- argument
Copy the code

Use

in code (1) to specify that the T type can only appear at the consumer’s location. Code (2) can be compiled with Any> String, but Container

can be assigned by Container

, which means that Container

is greater than Container

, That is, it looks like T directly implements the generic contravariant, without the need for
wildcard to implement inverting. For Java code, you need to write Container
c = new Container
(); .

This is the declarative variant. Use the out and in keywords when declaring a class, and write the code for the generic variant directly.

Java, on the other hand, must use wildcards in order to achieve generics, which is the use of the type.

Types of projection

Sometimes a class can act as both a producer and a consumer, in which case we cannot simply add the in or out keyword before T. Such as:

class Container<T> {
    private  var item: T? = null
    
    fun set(t: T?). {
        item = t
    }

    fun get(a): T? = item
}
Copy the code

Consider this function:

fun copy(from: Container<Any>, to: Container<Any>) {
    to.set(from.get()}Copy the code

When we actually use this function:

val from = Container<Int> ()val to = Container<Any>()
copy(from, to) > Container
      
       ; > Container
       
Copy the code

If we use this, the compiler will report an error because we are assigning two different types. In the words of kotlin’s official documentation, copy is “doing something bad” by trying to write a value of type Any to which we receive an Int. If the compiler does not report an error, the runtime will throw a ClassCastException.

So what should we do? Just prevent from being written!

Change the copy function to look like this:

fun copy(from: Container<out Any>, to: Container<Any>) { // Add out to the type from
    to.set(from.get()}val from = Container<Int> ()val to = Container<Any>()
copy(from, to) // No more errors will be reported
Copy the code

This is the type projection: From is a restricted (projected) Container class that we can only use as a producer, and it can only call the get() method.

Similarly, if the generic form from is modified with in, then from can only be used as a consumer, and it can only call the set() method, and the code above will report an error:

fun copy(from: Container<in Any>, to: Container<Any>) { // Add in to the type from
    to.set(from.get()}val from = Container<Int> ()val to = Container<Any>()
copy(from, to) / / an error
Copy the code

In fact, as you can see from the above, type projection is similar to the Java wildcard, which is also a type variant.

Why do we do this?

Why do Java arrays default and generics default? Kolin’s generic is also untyped by default, just using the out and in keywords to make it look generic.

Why? Why not default to generics?

Found the answer on stackoverflow, reference: stackoverflow.com/questions/1…

Both Java and C# had no generics feature in their early days.

But to support program polymorphism, arrays are designed to be covariant. Because a lot of the methods for arrays should work for arrays of all types of elements.

For example, here are two methods:

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);
Copy the code

The first one is to compare whether the array is equal; The second is shuffling the array order.

The designers of the language wanted these methods to be available for arrays of elements of any type. For example, I could call shuffleArray(String[] s) to get an array of strings out of order.

For this reason, arrays are designed to be covariant in both Java and C#.

With generics, however, there are the following problems:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());/ / (1)
Dog dog = dogs.get(0); //(2) This should be safe, right?
Copy the code

If the above code can be compiled, that is, List

can be assigned to List

, the List is covariant. Next add a Cat() to the List

, as in code (1). This could cause a mismatch between the type of the receiver Dog Dog in code (2) and the type of dogs. Get (0). A runtime exception is thrown. So Java prevents this behavior at compile time by making generics default.

conclusion

List<String> is not a subclass of List<Object>. If you want to implement a generic variant, you need the <? Extends T > and <? Super T> wildcard, which is a way to use the positional variant. Use the <? The extends T> wildcard means that the class is a producer and can only call methods like get(): T. And use the <? Super T> Wildcard means that the class is a consumer and can only call methods like set(T T), add(T T), and so on.

2. Kotlin generics are also untyped by default, but using the out and in keywords in the class declaration can make it look like a direct typecase when used. But this limits the class to being declared as either a producer or a consumer.

Using a type projection avoids class restrictions at declaration time, but uses the out and in keywords to indicate whether the class is playing the role of consumer or producer at this point in time. Type projection is also a method of using local typomorphism.