Home > Java > JavaBase > body text

Let's talk about kotlin's covariance and contravariance from Java

coldplay.xixi
Release: 2020-10-13 11:14:23
forward
2018 people have browsed it

java Basic Tutorial column introduces covariance and contravariance of kotlin today.

Let's talk about kotlin's covariance and contravariance from Java

Preface

In order to better understand covariance and contravariance in kotlin and Java, let’s first look at some basic knowledge.

Normal assignment

In Java, the common assignment statements are as follows:

A a = b;复制代码
Copy after login

The conditions that the assignment statement must meet are: the left side is either the parent class of the right side, or it is the same as the right side Same type. That is, the type of A must be "larger" than the type of B, such as Object o = new String("s");. For convenience, hereafter it is called A > B.

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

Assignment of function parameters

public void fun(A a) {}// 调用处赋值B b = new B();
fun(b);复制代码
Copy after login

When calling the fun(b) method, Assign the passed actual parameter B b to the formal parameter A a, that is, in the form A a = b. Similarly, the formal parameter type must be greater than the actual parameter, that is, A > B.

Assignment of function return value

public A fun() {
    B b = new B();    return b;
} 
复制代码
Copy after login

The function return value type receives the value of the actual return type. The actual return type B b is equivalent to assigning a value to the function return value type A a, that is, B b assignment Given A a, that is, A a = b, then the type relationship of A > B must be satisfied.

So, no matter what kind of assignment, it must satisfy the left type > right type, that is, A > B.

Covariance and contravariance in Java

With the previous basic knowledge, covariance and contravariance can be easily explained.

If class A > class B, trans(A) and trans(B) obtained after changing trans still satisfy trans(A) > trans(B), then it is called agreement Change.

Contravariation is just the opposite. If class A > class B, the trans(A) and trans(B) obtained after a change trans satisfy trans(B) > trans(A), which is called Inverter.

For example, everyone knows that Java arrays are covariant. If A > B, then A[] > B[], so B[] can be assigned to A[]. For example:

Integer[] nums = new Integer[]{};
Object[] o = nums; // 可以赋值,因为数组的协变特性所以由 Object > Integer 得到 Object[] > Integer[]复制代码
Copy after login

But Java's generics do not satisfy covariance, as follows:

List<Integer> l = new ArrayList<>();
List<Object> o = l;// 这里会报错,不能编译复制代码
Copy after login
Copy after login

The above code reports an error because, although Object > Integer, the generics do not satisfy covariance. , so List<Object> > List<Integer> is not satisfied. Since the condition that the left side is greater than the right side is not satisfied, we know from the preface that naturally List< Integer> is assigned to List<Object>. It is generally said that Java generics do not support type variation.

How to implement covariance and contravariance with generics in Java

We know from the previous point that generics in Java do not support type changes, but this will produce a very strange Doubts are also mentioned in many articles about generics:

If B is a subclass of A, then List should be a subclass of List! This is a very natural idea!

But sorry, Java does not support it for various reasons. However, Java does not completely obliterate the variable characteristics of generics. Java provides and to make generics have covariance and contravariance characteristics.

and

are called upper bound wildcards, are called lower bound wildcards. Use upper bound wildcards to make generics covariant, and use lower bound wildcards to make generics contravariant.

For example, in the previous example

If you use the upper bound wildcard,

In this way, the type of List will be larger than the type of List<Integer> , thus achieving covariance. This is what is called "a subclass of a generic is a subclass of a generic."

Similarly, the lower bound wildcard character can achieve inversion, such as:

How can the above code achieve inversion? First of all, Object > Integer; in addition, we know from the preface that the function return value type must be greater than the actual return value type, here it is List<? super Integer> > List<Object>, just the opposite of Object > Integer. In other words, after the generic change, the type relationship between Object and Integer is reversed. This is contravariance, and what implements contravariance is the lower bound wildcard .

As can be seen from the above, the upper bound in is T, which means that the types generally referred to by are all subclasses of T or T itself, so T is greater than . The lower bound in is T, that is to say, the types generally referred to by are the parent class of T or T itself, so is greater than T.

虽然 Java 使用通配符解决了泛型的协变与逆变的问题,但是由于很多讲到泛型的文章都晦涩难懂,曾经让我一度感慨这 tm 到底是什么玩意?直到我在 stackoverflow 上发现了通俗易懂的解释(是的,前文大部分内容都来自于 stackoverflow 中大神的解释),才终于了然。其实只要抓住赋值语句左边类型必须大于右边类型这个关键点一切就都很好懂了。

PECS

PECS 准则即 Producer Extends Consumer Super,生产者使用上界通配符,消费者使用下界通配符。直接看这句话可能会让人很疑惑,所以我们追本溯源来看看为什么会有这句话。

首先,我们写一个简单的泛型类:

然后写出如下代码:

代码 (1),Container<Object> c = new Container<String>(); 编译报错,因为泛型是不型变的,所以 Container 并不是 Container 的子类型,所以无法赋值。

代码 (2),加了上界通配符以后,支持泛型协变,Container 就成了 Container 的子类型,所以编译通过,可以赋值。

既然代码 (2) 通过编译,那代码 (3) 为什么会报错呢?因为代码 (3) 尝试把 String 类型赋值给 类型。显然,编译器只知道 是 Obejct 的某一个子类型,但是具体是哪一个并不知道,也许并不是 String 类型,所以不能直接将 String 类型赋值给它。

从上面可以看出,对于使用了 的类型,是不能写入元素的,不然就会像代码 (3) 处一样编译报错。

但是可以读取元素,比如代码 (4) 。并且该类型只能读取元素,这就是所谓的“生产者”,即只能从中读取元素的就是生产者,生产者就使用 通配符。

消费者同理,代码如下:

Container<String> c = new Container<Object>(); // (1)编译报错Container<? super String> c = new Container<Object>(); // (2)编译通过
 c.set("sss");// (3) 编译通过
 String s = c.get();// (4) 编译报错复制代码
Copy after login

代码 (1) 编译报错,因为泛型不支持逆变。而且就算不懂泛型,这个代码的形式一眼看起来也是错的。

代码 (2) 编译通过,因为加了 通配符后,泛型逆变。

代码 (3) 编译通过,它把 String 类型赋值给 泛指 String 的父类或 String,所以这是可以通过编译的。

代码 (4) 编译报错,因为它尝试把 赋值给 String,而 大于 String,所以不能赋值。事实上,编译器完全不知道该用什么类型去接受 c.get() 的返回值,因为在编译器眼里 是一个泛指的类型,所有 String 的父类和 String 本身都有可能。

同样从上面代码可以看出,对于使用了 的类型,是不能读取元素的,不然就会像代码 (4) 处一样编译报错。但是可以写入元素,比如代码 (3)。该类型只能写入元素,这就是所谓的“消费者”,即只能写入元素的就是消费者,消费者就使用 通配符。

综上,这就是 PECS 原则。

kotlin 中的协变与逆变

kotlin 抛弃了 Java 中的通配符,转而使用了声明处型变类型投影

声明处型变

首先让我们回头看看 Container 的定义:

public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}复制代码
Copy after login
Copy after login

在某些情况下,我们只会使用 Container<? extends T> 或者 Container<? super T> ,意味着我们只使用 Container 作为生产者或者 Container 作为消费者。

既然如此,那我们为什么要在定义 Container 这个类的时候要把 get 和 set 都定义好呢?试想一下,如果一个类只有消费者的作用,那定义 get 方法完全是多余的。

反过来说,如果一个泛型类只有生产者方法,比如下面这个例子(来自 kotlin 官方文档):

// Javainterface Source<T> {
  T nextT(); // 只有生产者方法}// Javavoid demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许,要使用上界通配符 <? extends Object>
  // ……}复制代码
Copy after login

Source<Object> 类型的变量中存储 Source<String> 实例的引用是极为安全的——因为没有消费者-方法可以调用。然而 Java 依然不让我们直接赋值,需要使用上界通配符。

但是这是毫无意义的,使用通配符只是把类型变得更复杂,并没有带来额外的价值,因为能调用的方法还是只有生产者方法。但 Java 编译器只认死理。

所以,如果我们能在使用之前确定一个类是生产者还是消费者,那在定义类的时候直接声明它的角色岂不美哉?

这就是 kotlin 的声明处型变,直接在类声明的时候,定义它的型变行为。

比如:

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

val c: Container<Any> = Container<String>()// (2)编译通过,因为 T 是一个 out-参数复制代码
Copy after login

(1) 处直接使用 指定 T 类型只能出现在生产者的位置上。虽然多了一些限制,但是,在 kotlin 编译器在知道了 T 的角色以后,就可以像 (2) 处一样将 Container 直接赋值给 Container,好像泛型直接可以协变了一样,而不需要再使用 Java 当中的通配符

同样的,对于消费者来说,

class Container<in T> { // (1) 
    private  var item: T? = null 
     fun set(t: T) {
        item = t
    }
}val c: Container<String> = Container<Any>() // (2) 编译通过,因为 T 是一个 in-参数复制代码
Copy after login

代码 (1) 处使用 指定 T 类型只能出现在消费者的位置上。代码 (2) 可以编译通过, Any > String,但是 Container 可以被 Container 赋值,意味着 Container 大于 Container ,即它看上去就像 T 直接实现了泛型逆变,而不需要借助 通配符来实现逆变。如果是 Java 代码,则需要写成 Container<? super String> c = new Container<Object>();

这就是声明处型变,在类声明的时候使用 out 和 in 关键字,在使用时可以直接写出泛型型变的代码。

而 Java 在使用时必须借助通配符才能实现泛型型变,这是使用处型变

类型投影

有时一个类既可以作生产者又可以作消费者,这种情况下,我们不能直接在 T 前面加 in 或者 out 关键字。比如:

class Container<T> {    private  var item: T? = null
    
    fun set(t: T?) {
        item = t
    }    fun get(): T? = item
}复制代码
Copy after login

考虑这个函数:

fun copy(from: Container<Any>, to: Container<Any>) {
    to.set(from.get())
}复制代码
Copy after login

当我们实际使用该函数时:

val from = Container<Int>()val to = Container<Any>()
copy(from, to) // 报错,from 是 Container<Int> 类型,而 to 是 Container<Any> 类型复制代码
Copy after login

Lets talk about kotlins covariance and contravariance from Java

这样使用的话,编译器报错,因为我们把两个不一样的类型做了赋值。用 kotlin 官方文档的话说,copy 函数在”干坏事“, 它尝试一个 Any 类型的值给 from, 而我们用 Int 类型来接收这个值,如果编译器不报错,那么运行时将会抛出一个 ClassCastException 异常。

所以应该怎么办?直接防止 from 被写入就可以了!

将 copy 函数改为如下所示:

fun copy(from: Container<out Any>, to: Container<Any>) { // 给 from 的类型加了 out
    to.set(from.get())
}val from = Container<Int>()val to = Container<Any>()
copy(from, to) // 不会再报错了复制代码
Copy after login

这就是类型投影:from 是一个类受限制的(投影的)Container 类,我们只能把它当作生产者来使用,它只能调用 get() 方法。

同理,如果 from 的泛型是用 in 来修饰的话,则 from 只能被当作消费者使用,它只能调用 set() 方法,上述代码就会报错:

fun copy(from: Container<in Any>, to: Container<Any>) { // 给 from 的类型加了 in
    to.set(from.get())
}val from = Container<Int>()val to = Container<Any>()
copy(from, to) //  报错复制代码
Copy after login

Lets talk about kotlins covariance and contravariance from Java

其实从上面可以看到,类型投影和 Java 的通配符很相似,也是一种使用时型变

为什么要这么设计?

为什么 Java 的数组是默认型变的,而泛型默认不型变呢?其实 kolin 的泛型默认也是不型变的,只是使用 out 和 in 关键字让它看起来像泛型型变。

为什么这么设计呢?为什么不默认泛型可型变呢?

在 stackoverflow 上找到了答案,参考:stackoverflow.com/questions/1…

Java 和 C# 早期都是没有泛型特性的。

但是为了支持程序的多态性,于是将数组设计成了协变的。因为数组的很多方法应该可以适用于所有类型元素的数组。

比如下面两个方法:

boolean equalArrays (Object[] a1, Object[] a2);void shuffleArray(Object[] a);复制代码
Copy after login

第一个是比较数组是否相等;第二个是打乱数组顺序。

语言的设计者们希望这些方法对于任何类型元素的数组都可以调用,比如我可以调用 shuffleArray(String[] s) 来把字符串数组的顺序打乱。

出于这样的考虑,在 Java 和 C# 中,数组设计成了协变的。

然而,对于泛型来说,却有以下问题:

// Illegal code - because otherwise life would be BadList<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awoogaanimals.add(new Cat());// (1)Dog dog = dogs.get(0); //(2) This should be safe, right?复制代码
Copy after login

如果上述代码可以通过编译,即 List 可以赋值给 List,List 是协变的。接下来往 List 中 add 一个 Cat(),如代码 (1) 处。这样就有可能造成代码 (2) 处的接收者 Dog dogdogs.get(0) 的类型不匹配的问题。会引发运行时的异常。所以 Java 在编译期就要阻止这种行为,把泛型设计为默认不型变的。

Summary

1. Java generics are not mutable by default, so List is not a subclass of List. If you want to implement generic type variation, you need extends T> and super T> wildcards, which is a way to use type variation. Using the extends T> wildcard means that the class is a producer and can only call methods like get(): T . Using the super T> wildcard means that the class is a consumer and can only call methods such as set(T t) and add(T t).

2. Kotlin generics are not variable by default. However, using the out and in keywords to change the type at the class declaration can achieve the effect of direct type change at the point of use. But this will limit the class to be either a producer or a consumer when declared.

Using type projection can avoid the class being restricted when it is declared, but when using it, you must use the out and in keywords to indicate whether the role of the class at this moment is a consumer or a producer. Type projection is also a method of using type changes.

Related free learning recommendations: java basic tutorial

The above is the detailed content of Let's talk about kotlin's covariance and contravariance from Java. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:juejin.im
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template