ホームページ > Java > &#&ベース > Java からの kotlin の共分散と反分散について話しましょう

Java からの kotlin の共分散と反分散について話しましょう

coldplay.xixi
リリース: 2020-10-13 11:14:23
転載
2063 人が閲覧しました

java Basic Tutorial コラムでは、今日の kotlin の共分散と反分散を紹介します。

Java からの kotlin の共分散と反分散について話しましょう

まえがき

Kotlin と Java の共分散と反分散をより深く理解するために、まず基本的な知識を見てみましょう。

通常の代入

Java では、一般的な代入ステートメントは次のとおりです:

A a = b;复制代码
ログイン後にコピー

代入ステートメントが満たさなければならない条件は次のとおりです: 左側が親クラスのいずれかである右側の、または右側と同じです。 同じタイプです。つまり、Object o = new String("s"); のように、A の型は B の型より「大きく」なければなりません。便宜上、以下ではA>Bと呼びます。

上記の最も一般的な代入ステートメントに加えて、他に 2 つの代入ステートメントがあります。

関数パラメータの代入

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 における共分散と反変性

これまでの基本的な知識があれば、共分散と反変性は簡単に説明できます。

class A > class 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 > List は満たされていません。左辺が右辺よりも大きいという条件が満たされていないため、序文から当然次のことがわかります。 List<Integer> が List<Object> に割り当てられます。一般に、Java ジェネリックスは型のバリエーションをサポートしないと言われています。

Java でジェネリックスとの共分散と反変性を実装する方法

前の時点から、Java のジェネリックスが型の変更をサポートしていないことはわかっていますが、これにより非常に奇妙な問題が発生します。ジェネリックに関する多くの記事:

B が A のサブクラスの場合、List は List のサブクラスである必要があります。これはとても自然な考え方ですね!

しかし、申し訳ありませんが、Java はさまざまな理由によりこれをサポートしていません。ただし、Java はジェネリックスの変数特性を完全に消去するわけではなく、ジェネリックスに共分散特性と反分散特性を持たせるための および を提供しています。

および

は上限ワイルドカードと呼ばれ、 は下限ワイルドカードと呼ばれます。ジェネリックスを共変にするには上限ワイルドカードを使用し、ジェネリックスを反変にするには下限ワイルドカードを使用します。

たとえば、前の例では、

List<Integer> l = new ArrayList<>();
List<Object> o = l;// 这里会报错,不能编译复制代码
ログイン後にコピー
ログイン後にコピー

上限ワイルドカードを使用すると、

List<Integer> l = new ArrayList<>();
List<? extends Object> o = l;// 可以通过编译复制代码
ログイン後にコピー

このように、List の型は次のようになります。 List の型より大きいため、共分散が達成されます。これは、「ジェネリックのサブクラスはジェネリックのサブクラスである」と呼ばれるものです。

同様に、下限のワイルドカード文字 は、次のような反転を実現できます:

public List<? super Integer> fun(){
    List<Object> l = new ArrayList<>();    return l;
}复制代码
ログイン後にコピー

上記のコードはどのようにして反転を実現できますか?まず、Object > Integer; さらに、関数の戻り値の型は実際の戻り値の型より大きくなければならないことが前文からわかりますが、ここでは

List > です。 List、Object > Integer の逆です。つまり、一般的な変更後、Object と Integer の間の型の関係が逆転します。これが反変性であり、反変性を実装するのは下限ワイルドカード です。

上記からわかるように、 の上限は T であり、一般に で参照される型はすべて T のサブクラスまたは 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 のサブクラスではありません。ジェネリック型バリエーションを実装したい場合は、型バリエーションを使用する方法である extends T> および super T> ワイルドカードが必要です。 extends T> ワイルドカードを使用すると、クラスがプロデューサーであり、 get(): T のようなメソッドのみを呼び出すことができます。 super T> ワイルドカードを使用すると、クラスがコンシューマーであり、set(T t) や add(T t) などのメソッドのみを呼び出すことができることを意味します。

2. Kotlin ジェネリックはデフォルトでは変数ではありませんが、out および in キーワードを使用してクラス宣言時に型を変更すると、使用時に型を直接変更する効果を得ることができます。ただし、これにより、宣言時にクラスがプロデューサーまたはコンシューマーのいずれかに制限されます。

型射影を使用すると、宣言時にクラスが制限されることを回避できますが、これを使用する場合は、out キーワードと in キーワードを使用して、現時点でのクラスの役割がコンシューマーであるかプロデューサであるかを示す必要があります。 。型投影も型変更を使用する方法です。

関連する無料学習の推奨事項: Java 基本チュートリアル

以上がJava からの kotlin の共分散と反分散について話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート