> Java > Java베이스 > 본문

Java의 Kotlin 공분산과 반공분산에 대해 이야기해 보겠습니다.

coldplay.xixi
풀어 주다: 2020-10-13 11:14:23
앞으로
1928명이 탐색했습니다.

Java 기본 튜토리얼칼럼에서는 오늘 Kotlin의 공분산과 반공분산을 소개합니다.

Java의 Kotlin 공분산과 반공분산에 대해 이야기해 보겠습니다.

서문

Kotlin과 Java의 공분산과 반공분산을 더 잘 이해하기 위해 먼저 몇 가지 기본 지식을 살펴보겠습니다.

일반 할당

Java에서 공통 할당문은 다음과 같습니다.

A a = b;复制代码
로그인 후 복사

대입문이 충족해야 하는 조건은 다음과 같습니다. 왼쪽이 오른쪽의 상위 클래스이거나 오른쪽과 동일한 유형입니다. 옆. 즉, A 유형은 Object o = new String("s"); 와 같이 B 유형보다 "더 커야" 합니다. 이하에서는 편의상 A> Object o = new String("s"); 。为了方便起见,下文中称作 A > B。

除了上述最常见的赋值语句,还有两种其他的赋值语句:

函数参数的赋值

public void fun(A a) {}// 调用处赋值B b = new B();
fun(b);复制代码
로그인 후 복사

在调用 fun(b) 方法时,会将传入的 B b 实参赋值给形参 A a,即 A a = b 的形式。同样的,必须要满足形参类型大于实参,即 A > B。

函数返回值的赋值

public A fun() {
    B b = new B();    return b;
} 
复制代码
로그인 후 복사

函数返回值类型接收实际返回类型的值,实际返回类型 B b 相当于赋值给了函数返回值类型 A a,即 B b 赋值给了 A a, 即 A a = b,那么必须满足 A > B 的类型关系。

所以,无论哪种赋值,都必须满足左边类型 > 右边类型,即 A > B。

Java 中的协变与逆变

有了前面的基础知识,就可以方便地解释协变与逆变了。

如果类 A > 类 B,经过一个变化 trans 后得到的 trans(A) 与 trans(B) 依旧满足 trans(A) > trans(B),那么称为协变

逆变则刚好相反,如果类 A > 类 B,经过一个变化 trans 后得到的 trans(A) 与 trans(B) 满足 trans(B) > trans(A),称为逆变

比如大家都知道 Java 的数组是协变的,假如 A > B,那么有 A[] > B[],所以 B[] 可以赋值给 A[]。举个例子:

Integer[] nums = new Integer[]{};
Object[] o = nums; // 可以赋值,因为数组的协变特性所以由 Object > Integer 得到 Object[] > Integer[]复制代码
로그인 후 복사

但是 Java 的泛型则不满足协变,如下:

List<Integer> l = new ArrayList<>();
List<Object> o = l;// 这里会报错,不能编译复制代码
로그인 후 복사
로그인 후 복사

上述代码报错,就是因为,虽然 Object > Integer,但是由于泛型不满足协变,所以 List<Object> > List<Integer> 是不满足的,既然不满足左边大于右边这个条件,从前言中我们知道,自然就不能将 List<Integer> 赋值给 List<Object>。一般称 Java 泛型不支持型变。

Java 中泛型如何实现协变与逆变

从前面我们知道,在 Java 中泛型是不支持型变的,但是这会产生一个让人很奇怪的疑惑,也是很多讲泛型的文章中提到的:

如果 B 是 A 的子类,那么 List 就应该是 List 的子类呀!这是一个非常自然而然的想法!

但是很抱歉,由于种种原因,Java 并不支持。但是,Java 并不是完全抹杀了泛型的型变特性,Java 提供了 使泛型拥有协变和逆变的特性。

称为上界通配符, 称为下界通配符。使用上界通配符可以使泛型协变,而使用下界通配符可以使泛型逆变。

比如之前举的例子

如果使用上界通配符,

这样,List 的类型就大于 List<Integer> 的类型了,也就实现了协变。这也就是所谓的“子类的泛型是泛型的子类”。

同样,下界通配符 可以实现逆变,如:

上述代码怎么就实现逆变了呢?首先,Object > Integer;另外,从前言我们知道,函数返回值类型必须大于实际返回值类型,在这里就是 List<? super Integer> > List<Object>

위에 언급된 가장 일반적인 할당 문 외에도 두 가지 다른 할당 문이 있습니다:

함수 매개 변수 할당

public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}复制代码
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
🎜fun(b) 메서드를 호출할 때, 전달된 실제 매개변수 B b 는 형식 매개변수 A a 에, 즉 A a = b 형식으로 할당됩니다. 마찬가지로 형식 매개변수 유형은 실제 매개변수보다 커야 합니다. 즉, A > B입니다. 🎜

함수 반환값 지정🎜
Container<Object> c = new Container<String>(); // (1)编译报错Container<? extends Object> c = new Container<String>(); // (2)编译通过c.set("sss"); // (3)编译报错Object o = c.get();// (4)编译通过复制代码
로그인 후 복사
로그인 후 복사
🎜함수 반환값 형식은 실제 반환값 형식의 값을 받습니다. B b 실제 반환값 형식은 함수 반환값 형식을 지정하는 것과 동일합니다. A a, 즉 B b 가 A a, 즉 A a = b에 할당되면 A > B의 유형 관계가 충족되어야 합니다. 🎜🎜그러므로 어떤 종류의 과제라도 left type > right type, 즉 A > B를 만족해야 합니다. 🎜

Java의 공분산과 반공변성🎜🎜이전의 기본 지식으로 공분산과 반공변성을 쉽게 설명할 수 있습니다. 🎜🎜클래스 A > 클래스 B, trans 변경 후에도 얻은 trans(A) 및 trans(B)가 여전히 trans(A) > trans(B)를 만족하는 경우 공변성이라고 합니다. . 🎜🎜역변동은 정반대인 경우, 변경 trans(A)와 trans(B)는 trans(B) > trans(A)를 만족합니다. 역변화. 🎜🎜예를 들어, Java 배열이 공변적이라는 것은 누구나 알고 있습니다. A > B이면 A[] > B[]가 A[]에 할당될 수 있습니다. 예: 🎜
Container<String> c = new Container<Object>(); // (1)编译报错Container<? super String> c = new Container<Object>(); // (2)编译通过
 c.set("sss");// (3) 编译通过
 String s = c.get();// (4) 编译报错复制代码
로그인 후 복사
로그인 후 복사
🎜그러나 Java의 제네릭은 다음과 같이 공분산을 충족하지 않습니다. 🎜
public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}复制代码
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
🎜위 코드는 Object > Integer임에도 불구하고 제네릭이 공분산을 충족하지 않기 때문에 오류를 보고합니다. 따라서 List > List<Integer>는 왼쪽이 오른쪽보다 크다는 조건을 만족하지 않으므로 당연히 List<Integer>를 할당할 수 없다는 것을 서문에서 알 수 있습니다. 목록< 일반적으로 Java 제네릭은 유형 변형을 지원하지 않는다고 합니다. 🎜

Java에서 제네릭으로 공분산과 반공분산을 구현하는 방법🎜🎜앞서 우리는 Java의 제네릭이 유형 변형을 지원하지 않는다는 것을 알고 있지만 이로 인해 사람들이 혼란스러워지는 문제가 발생합니다. . 제네릭에 관한 많은 기사에서도 언급되는 매우 이상한 의심입니다. 🎜🎜B가 A의 하위 클래스라면 List는 List의 하위 클래스여야 합니다! 이것은 매우 자연스러운 생각입니다! 🎜🎜하지만 안타깝게도 Java에서는 여러 가지 이유로 이를 지원하지 않습니다. 그러나 Java는 제네릭의 가변 특성을 완전히 제거하지 않습니다. Java는 제네릭이 공분산 및 반공분산 특성을 갖도록 를 제공합니다. 🎜

🎜🎜는 상한 와일드카드라고 하며,
// Javainterface Source<T> {
  T nextT(); // 只有生产者方法}// Javavoid demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许,要使用上界通配符 <? extends Object>
  // ……}复制代码
로그인 후 복사
로그인 후 복사

🎜상한 와일드카드를 사용하면 🎜
class Container<out T> { // (1)
    private  var item: T? = null 
        
    fun get(): T? = item
}

val c: Container<Any> = Container<String>()// (2)编译通过,因为 T 是一个 out-参数复制代码
로그인 후 복사
로그인 후 복사
🎜이렇게 하면 List 유형이 List<Integer>보다 커집니다. 공분산. 이것이 바로 "제네릭의 서브클래스는 제네릭의 서브클래스이다"라는 것입니다. 🎜🎜마찬가지로, 하한 와일드카드 문자 는 다음과 같이 반전을 달성할 수 있습니다. 🎜
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-参数复制代码
로그인 후 복사
로그인 후 복사
🎜위 코드는 어떻게 반전을 달성할 수 있나요? 우선, Object > Integer; 또한 함수 반환 값 유형이 실제 반환 값 유형보다 커야 한다는 것을 서문에서 알 수 있습니다. 여기서는 List<super Integer> >입니다. ; List<Object>, Object > 즉, 제네릭 변경 후에는 Object와 Integer 간의 유형 관계가 역전되는데, 이것이 반공변성을 구현하는 것은 하한 와일드카드 입니다. 🎜🎜위에서 볼 수 있듯이 의 상한은 T입니다. 이는 일반적으로 에서 참조되는 유형이 모두 T 또는 T 자체라는 것을 의미합니다. 보다 크다. 의 하한은 T입니다. 즉, 일반적으로 가 참조하는 유형은 T 또는 T 자체입니다. 티. 🎜

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

PECS

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

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

public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}复制代码
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

然后写出如下代码:

Container<Object> c = new Container<String>(); // (1)编译报错Container<? extends Object> c = new Container<String>(); // (2)编译通过c.set("sss"); // (3)编译报错Object o = c.get();// (4)编译通过复制代码
로그인 후 복사
로그인 후 복사

代码 (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) 编译报错复制代码
로그인 후 복사
로그인 후 복사

代码 (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;
    }
}复制代码
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

在某些情况下,我们只会使用 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>
  // ……}复制代码
로그인 후 복사
로그인 후 복사

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-参数复制代码
로그인 후 복사
로그인 후 복사

(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-参数复制代码
로그인 후 복사
로그인 후 복사

代码 (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
}复制代码
로그인 후 복사

考虑这个函数:

fun copy(from: Container<Any>, to: Container<Any>) {
    to.set(from.get())
}复制代码
로그인 후 복사

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

val from = Container<Int>()val to = Container<Any>()
copy(from, to) // 报错,from 是 Container<Int> 类型,而 to 是 Container<Any> 类型复制代码
로그인 후 복사

Java의 Kotlin 공분산과 반공분산에 대해 이야기해 보겠습니다.

这样使用的话,编译器报错,因为我们把两个不一样的类型做了赋值。用 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) // 不会再报错了复制代码
로그인 후 복사

这就是类型投影: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) //  报错复制代码
로그인 후 복사

Java의 Kotlin 공분산과 반공분산에 대해 이야기해 보겠습니다.

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

为什么要这么设计?

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

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

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

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

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

比如下面两个方法:

boolean equalArrays (Object[] a1, Object[] a2);void shuffleArray(Object[] a);复制代码
로그인 후 복사

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

语言的设计者们希望这些方法对于任何类型元素的数组都可以调用,比如我可以调用 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?复制代码
로그인 후 복사

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

요약

1. Java 제네릭은 기본적으로 변경할 수 없으므로 List은 List의 하위 클래스가 아닙니다. 일반 유형 변형을 구현하려면 유형 변형을 사용하는 방법인 확장 T> 확장 T> 와일드카드를 사용하면 클래스가 생성자이며 get(): T 와 같은 메서드만 호출할 수 있습니다. super T> 와일드카드를 사용한다는 것은 클래스가 소비자이며 set(T t) 및 add(T t)와 같은 메서드만 호출할 수 있음을 의미합니다.

2. Kotlin 제네릭은 기본적으로 변수가 아닙니다. 그러나 클래스 선언에서 유형을 변경하기 위해 out 및 in 키워드를 사용하면 사용 시점에서 직접 유형 변경 효과를 얻을 수 있습니다. 그러나 이렇게 하면 선언 시 클래스가 생산자 또는 소비자로 제한됩니다.

타입 프로젝션을 사용하면 클래스 선언 시 제한을 피할 수 있지만, 사용할 때는 out 및 in 키워드를 사용하여 현재 클래스의 역할이 소비자인지 생산자인지 표시해야 합니다. 유형 투영은 유형 변경을 사용하는 방법이기도 합니다.

관련 무료 학습 권장사항: Java 기본 튜토리얼

위 내용은 Java의 Kotlin 공분산과 반공분산에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.im
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!