内射老阿姨1区2区3区4区_久久精品人人做人人爽电影蜜月_久久国产精品亚洲77777_99精品又大又爽又粗少妇毛片

Java中kotlin的協(xié)變與逆變是什么-創(chuàng)新互聯(lián)

小編給大家分享一下Java中kotlin的協(xié)變與逆變是什么,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!

云陽(yáng)ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書未來(lái)市場(chǎng)廣闊!成為創(chuàng)新互聯(lián)公司的ssl證書銷售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:028-86922220(備注:SSL證書合作)期待與您的合作!

java基礎(chǔ)教程欄目今天介紹kotlin的協(xié)變與逆變。

前言

為了更好地理解 kotlin 和 Java 中的協(xié)變與逆變,先看一些基礎(chǔ)知識(shí)。

普通賦值

在 Java 中,常見(jiàn)的賦值語(yǔ)句如下:

A a = b;復(fù)制代碼

賦值語(yǔ)句必須滿足的條件是:左邊要么是右邊的父類,要么和右邊類型一樣。即 A 的類型要“大于”B 的類型,比如 Object o = new String("s"); 。為了方便起見(jiàn),下文中稱作 A > B。

除了上述最常見(jiàn)的賦值語(yǔ)句,還有兩種其他的賦值語(yǔ)句:

函數(shù)參數(shù)的賦值
public void fun(A a) {}// 調(diào)用處賦值B b = new B();
fun(b);復(fù)制代碼

在調(diào)用 fun(b) 方法時(shí),會(huì)將傳入的 B b 實(shí)參賦值給形參 A a,即 A a = b 的形式。同樣的,必須要滿足形參類型大于實(shí)參,即 A > B。

函數(shù)返回值的賦值
public A fun() {
    B b = new B();    return b;
} 
復(fù)制代碼

函數(shù)返回值類型接收實(shí)際返回類型的值,實(shí)際返回類型 B b 相當(dāng)于賦值給了函數(shù)返回值類型 A a,即 B b 賦值給了 A a, 即 A a = b,那么必須滿足 A > B 的類型關(guān)系。

所以,無(wú)論哪種賦值,都必須滿足左邊類型 > 右邊類型,即 A > B。

Java 中的協(xié)變與逆變

有了前面的基礎(chǔ)知識(shí),就可以方便地解釋協(xié)變與逆變了。

如果類 A > 類 B,經(jīng)過(guò)一個(gè)變化 trans 后得到的 trans(A) 與 trans(B) 依舊滿足 trans(A) > trans(B),那么稱為協(xié)變。

逆變則剛好相反,如果類 A > 類 B,經(jīng)過(guò)一個(gè)變化 trans 后得到的 trans(A) 與 trans(B) 滿足 trans(B) > trans(A),稱為逆變。

比如大家都知道 Java 的數(shù)組是協(xié)變的,假如 A > B,那么有 A[] > B[],所以 B[] 可以賦值給 A[]。舉個(gè)例子:

Integer[] nums = new Integer[]{};
Object[] o = nums; // 可以賦值,因?yàn)閿?shù)組的協(xié)變特性所以由 Object > Integer 得到 Object[] > Integer[]復(fù)制代碼

但是 Java 的泛型則不滿足協(xié)變,如下:

List<Integer> l = new ArrayList<>();
List<Object> o = l;// 這里會(huì)報(bào)錯(cuò),不能編譯復(fù)制代碼

上述代碼報(bào)錯(cuò),就是因?yàn)?,雖然 Object > Integer,但是由于泛型不滿足協(xié)變,所以 List<Object> > List<Integer> 是不滿足的,既然不滿足左邊大于右邊這個(gè)條件,從前言中我們知道,自然就不能將 List<Integer> 賦值給 List<Object>。一般稱 Java 泛型不支持型變。

Java 中泛型如何實(shí)現(xiàn)協(xié)變與逆變

從前面我們知道,在 Java 中泛型是不支持型變的,但是這會(huì)產(chǎn)生一個(gè)讓人很奇怪的疑惑,也是很多講泛型的文章中提到的:

如果 B 是 A 的子類,那么 List<B> 就應(yīng)該是 List<A> 的子類呀!這是一個(gè)非常自然而然的想法!

但是很抱歉,由于種種原因,Java 并不支持。但是,Java 并不是完全抹殺了泛型的型變特性,Java 提供了 <? extends T> 和 <? super T> 使泛型擁有協(xié)變和逆變的特性。

<? extends T> 與 <? super T>

<? extends T> 稱為上界通配符,<? super T> 稱為下界通配符。使用上界通配符可以使泛型協(xié)變,而使用下界通配符可以使泛型逆變。

比如之前舉的例子

List<Integer> l = new ArrayList<>();
List<Object> o = l;// 這里會(huì)報(bào)錯(cuò),不能編譯復(fù)制代碼

如果使用上界通配符,

List<Integer> l = new ArrayList<>();
List<? extends Object> o = l;// 可以通過(guò)編譯復(fù)制代碼

這樣,List<? extends Object> 的類型就大于 List<Integer> 的類型了,也就實(shí)現(xiàn)了協(xié)變。這也就是所謂的“子類的泛型是泛型的子類”。

同樣,下界通配符 <? super T> 可以實(shí)現(xiàn)逆變,如:

public List<? super Integer> fun(){
    List<Object> l = new ArrayList<>();    return l;
}復(fù)制代碼

上述代碼怎么就實(shí)現(xiàn)逆變了呢?首先,Object > Integer;另外,從前言我們知道,函數(shù)返回值類型必須大于實(shí)際返回值類型,在這里就是 List<? super Integer> > List<Object>,和 Object > Integer 剛好相反。也就是說(shuō),經(jīng)過(guò)泛型變化后,Object 和 Integer 的類型關(guān)系翻轉(zhuǎn)了,這就是逆變,而實(shí)現(xiàn)逆變的就是下界通配符 <? super T>。

從上面可以看出,<? extends T> 中的上界是 T,也就是說(shuō) <? extends T> 所泛指的類型都是 T 的子類或 T 本身,所以 T 大于 <? extends T> 。<? super T> 中的下界是 T,也就是說(shuō) <? super T> 所泛指的類型都是 T 的父類或 T 本身,所以 <? super T>  大于 T。

雖然 Java 使用通配符解決了泛型的協(xié)變與逆變的問(wèn)題,但是由于很多講到泛型的文章都晦澀難懂,曾經(jīng)讓我一度感慨這 tm 到底是什么玩意?直到我在 stackoverflow 上發(fā)現(xiàn)了通俗易懂的解釋(是的,前文大部分內(nèi)容都來(lái)自于 stackoverflow 中大神的解釋),才終于了然。其實(shí)只要抓住賦值語(yǔ)句左邊類型必須大于右邊類型這個(gè)關(guān)鍵點(diǎn)一切就都很好懂了。

PECS

PECS 準(zhǔn)則即 Producer Extends Consumer Super,生產(chǎn)者使用上界通配符,消費(fèi)者使用下界通配符。直接看這句話可能會(huì)讓人很疑惑,所以我們追本溯源來(lái)看看為什么會(huì)有這句話。

首先,我們寫一個(gè)簡(jiǎn)單的泛型類:

public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}復(fù)制代碼

然后寫出如下代碼:

Container<Object> c = new Container<String>(); // (1)編譯報(bào)錯(cuò)Container<? extends Object> c = new Container<String>(); // (2)編譯通過(guò)c.set("sss"); // (3)編譯報(bào)錯(cuò)Object o = c.get();// (4)編譯通過(guò)復(fù)制代碼

代碼 (1),Container<Object> c = new Container<String>(); 編譯報(bào)錯(cuò),因?yàn)榉盒褪遣恍妥兊模?Container<String> 并不是 Container<Object> 的子類型,所以無(wú)法賦值。

代碼 (2),加了上界通配符以后,支持泛型協(xié)變,Container<String> 就成了 Container<? extends Object> 的子類型,所以編譯通過(guò),可以賦值。

既然代碼 (2) 通過(guò)編譯,那代碼 (3) 為什么會(huì)報(bào)錯(cuò)呢?因?yàn)榇a (3) 嘗試把 String 類型賦值給 <? extends Object> 類型。顯然,編譯器只知道 <? extends Object>  是 Obejct 的某一個(gè)子類型,但是具體是哪一個(gè)并不知道,也許并不是 String 類型,所以不能直接將 String 類型賦值給它。

從上面可以看出,對(duì)于使用了 <? extends T> 的類型,是不能寫入元素的,不然就會(huì)像代碼 (3) 處一樣編譯報(bào)錯(cuò)。

但是可以讀取元素,比如代碼 (4) 。并且該類型只能讀取元素,這就是所謂的“生產(chǎn)者”,即只能從中讀取元素的就是生產(chǎn)者,生產(chǎn)者就使用 <? extends T> 通配符。

消費(fèi)者同理,代碼如下:

Container<String> c = new Container<Object>(); // (1)編譯報(bào)錯(cuò)Container<? super String> c = new Container<Object>(); // (2)編譯通過(guò)
 c.set("sss");// (3) 編譯通過(guò)
 String s = c.get();// (4) 編譯報(bào)錯(cuò)復(fù)制代碼

代碼 (1) 編譯報(bào)錯(cuò),因?yàn)榉盒筒恢С帜孀儭6揖退悴欢盒?,這個(gè)代碼的形式一眼看起來(lái)也是錯(cuò)的。

代碼 (2) 編譯通過(guò),因?yàn)榧恿?<? super T> 通配符后,泛型逆變。

代碼 (3) 編譯通過(guò),它把 String 類型賦值給 <? super String>,<? super String> 泛指 String 的父類或 String,所以這是可以通過(guò)編譯的。

代碼 (4) 編譯報(bào)錯(cuò),因?yàn)樗鼑L試把 <? super String> 賦值給 String,而 <? super String> 大于 String,所以不能賦值。事實(shí)上,編譯器完全不知道該用什么類型去接受 c.get() 的返回值,因?yàn)樵诰幾g器眼里 <? super String> 是一個(gè)泛指的類型,所有 String 的父類和 String 本身都有可能。

同樣從上面代碼可以看出,對(duì)于使用了 <? super T> 的類型,是不能讀取元素的,不然就會(huì)像代碼 (4) 處一樣編譯報(bào)錯(cuò)。但是可以寫入元素,比如代碼 (3)。該類型只能寫入元素,這就是所謂的“消費(fèi)者”,即只能寫入元素的就是消費(fèi)者,消費(fèi)者就使用 <? super T> 通配符。

綜上,這就是 PECS 原則。

kotlin 中的協(xié)變與逆變

kotlin 拋棄了 Java 中的通配符,轉(zhuǎn)而使用了聲明處型變類型投影

聲明處型變

首先讓我們回頭看看 Container 的定義:

public class Container<T> {    private T item;    public void set(T t) { 
        item = t;
    }    public T get() {        return item;
    }
}復(fù)制代碼

在某些情況下,我們只會(huì)使用 Container<? extends T> 或者 Container<? super T> ,意味著我們只使用 Container 作為生產(chǎn)者或者 Container 作為消費(fèi)者。

既然如此,那我們?yōu)槭裁匆诙x Container 這個(gè)類的時(shí)候要把 get 和 set 都定義好呢?試想一下,如果一個(gè)類只有消費(fèi)者的作用,那定義 get 方法完全是多余的。

反過(guò)來(lái)說(shuō),如果一個(gè)泛型類只有生產(chǎn)者方法,比如下面這個(gè)例子(來(lái)自 kotlin 官方文檔):

// Javainterface Source<T> {
  T nextT(); // 只有生產(chǎn)者方法}// Javavoid demo(Source<String> strs) {
  Source<Object> objects = strs; // !?。≡?Java 中不允許,要使用上界通配符 <? extends Object>
  // ……}復(fù)制代碼

Source<Object> 類型的變量中存儲(chǔ) Source<String> 實(shí)例的引用是極為安全的——因?yàn)闆](méi)有消費(fèi)者-方法可以調(diào)用。然而 Java 依然不讓我們直接賦值,需要使用上界通配符。

但是這是毫無(wú)意義的,使用通配符只是把類型變得更復(fù)雜,并沒(méi)有帶來(lái)額外的價(jià)值,因?yàn)槟苷{(diào)用的方法還是只有生產(chǎn)者方法。但 Java 編譯器只認(rèn)死理。

所以,如果我們能在使用之前確定一個(gè)類是生產(chǎn)者還是消費(fèi)者,那在定義類的時(shí)候直接聲明它的角色豈不美哉?

這就是 kotlin 的聲明處型變,直接在類聲明的時(shí)候,定義它的型變行為。

比如:

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

val c: Container<Any> = Container<String>()// (2)編譯通過(guò),因?yàn)?T 是一個(gè) out-參數(shù)復(fù)制代碼

(1) 處直接使用 <out T> 指定 T 類型只能出現(xiàn)在生產(chǎn)者的位置上。雖然多了一些限制,但是,在 kotlin 編譯器在知道了 T 的角色以后,就可以像 (2) 處一樣將 Container<String> 直接賦值給 Container<Any>,好像泛型直接可以協(xié)變了一樣,而不需要再使用 Java 當(dāng)中的通配符 <? extends String>。

同樣的,對(duì)于消費(fèi)者來(lái)說(shuō),

class Container<in T> { // (1) 
    private  var item: T? = null 
     fun set(t: T) {
        item = t
    }
}val c: Container<String> = Container<Any>() // (2) 編譯通過(guò),因?yàn)?T 是一個(gè) in-參數(shù)復(fù)制代碼

代碼 (1) 處使用 <in T> 指定 T 類型只能出現(xiàn)在消費(fèi)者的位置上。代碼 (2) 可以編譯通過(guò), Any > String,但是 Container<String> 可以被 Container<Any> 賦值,意味著 Container<String> 大于 Container<Any> ,即它看上去就像 T 直接實(shí)現(xiàn)了泛型逆變,而不需要借助 <? super String> 通配符來(lái)實(shí)現(xiàn)逆變。如果是 Java 代碼,則需要寫成 Container<? super String> c = new Container<Object>();

這就是聲明處型變,在類聲明的時(shí)候使用 out 和 in 關(guān)鍵字,在使用時(shí)可以直接寫出泛型型變的代碼。

而 Java 在使用時(shí)必須借助通配符才能實(shí)現(xiàn)泛型型變,這是使用處型變

類型投影

有時(shí)一個(gè)類既可以作生產(chǎn)者又可以作消費(fèi)者,這種情況下,我們不能直接在 T 前面加 in 或者 out 關(guān)鍵字。比如:

class Container<T> {    private  var item: T? = null
    
    fun set(t: T?) {
        item = t
    }    fun get(): T? = item
}復(fù)制代碼

考慮這個(gè)函數(shù):

fun copy(from: Container<Any>, to: Container<Any>) {
    to.set(from.get())
}復(fù)制代碼

當(dāng)我們實(shí)際使用該函數(shù)時(shí):

val from = Container<Int>()val to = Container<Any>()
copy(from, to) // 報(bào)錯(cuò),from 是 Container<Int> 類型,而 to 是 Container<Any> 類型復(fù)制代碼

Java中kotlin的協(xié)變與逆變是什么

這樣使用的話,編譯器報(bào)錯(cuò),因?yàn)槲覀儼褍蓚€(gè)不一樣的類型做了賦值。用 kotlin 官方文檔的話說(shuō),copy 函數(shù)在”干壞事“, 它嘗試一個(gè) Any 類型的值給 from, 而我們用 Int 類型來(lái)接收這個(gè)值,如果編譯器不報(bào)錯(cuò),那么運(yùn)行時(shí)將會(huì)拋出一個(gè) ClassCastException 異常。

所以應(yīng)該怎么辦?直接防止 from 被寫入就可以了!

將 copy 函數(shù)改為如下所示:

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) // 不會(huì)再報(bào)錯(cuò)了復(fù)制代碼

這就是類型投影:from 是一個(gè)類受限制的(投影的)Container 類,我們只能把它當(dāng)作生產(chǎn)者來(lái)使用,它只能調(diào)用 get() 方法。

同理,如果 from 的泛型是用 in 來(lái)修飾的話,則 from 只能被當(dāng)作消費(fèi)者使用,它只能調(diào)用 set() 方法,上述代碼就會(huì)報(bào)錯(cuò):

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) //  報(bào)錯(cuò)復(fù)制代碼

Java中kotlin的協(xié)變與逆變是什么

其實(shí)從上面可以看到,類型投影和 Java 的通配符很相似,也是一種使用時(shí)型變

為什么要這么設(shè)計(jì)?

為什么 Java 的數(shù)組是默認(rèn)型變的,而泛型默認(rèn)不型變呢?其實(shí) kolin 的泛型默認(rèn)也是不型變的,只是使用 out 和 in 關(guān)鍵字讓它看起來(lái)像泛型型變。

為什么這么設(shè)計(jì)呢?為什么不默認(rèn)泛型可型變呢?

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

Java 和 C# 早期都是沒(méi)有泛型特性的。

但是為了支持程序的多態(tài)性,于是將數(shù)組設(shè)計(jì)成了協(xié)變的。因?yàn)閿?shù)組的很多方法應(yīng)該可以適用于所有類型元素的數(shù)組。

比如下面兩個(gè)方法:

boolean equalArrays (Object[] a1, Object[] a2);void shuffleArray(Object[] a);復(fù)制代碼

第一個(gè)是比較數(shù)組是否相等;第二個(gè)是打亂數(shù)組順序。

語(yǔ)言的設(shè)計(jì)者們希望這些方法對(duì)于任何類型元素的數(shù)組都可以調(diào)用,比如我可以調(diào)用 shuffleArray(String[] s) 來(lái)把字符串?dāng)?shù)組的順序打亂。

出于這樣的考慮,在 Java 和 C# 中,數(shù)組設(shè)計(jì)成了協(xié)變的。

然而,對(duì)于泛型來(lái)說(shuō),卻有以下問(wèn)題:

// 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?復(fù)制代碼

如果上述代碼可以通過(guò)編譯,即 List<Dog> 可以賦值給 List<Animal>,List 是協(xié)變的。接下來(lái)往 List<Dog> 中 add 一個(gè) Cat(),如代碼 (1) 處。這樣就有可能造成代碼 (2) 處的接收者 Dog dogdogs.get(0) 的類型不匹配的問(wèn)題。會(huì)引發(fā)運(yùn)行時(shí)的異常。所以 Java 在編譯期就要阻止這種行為,把泛型設(shè)計(jì)為默認(rèn)不型變的。

總結(jié)

1、Java 泛型默認(rèn)不型變,所以 List<String> 不是 List<Object> 的子類。如果要實(shí)現(xiàn)泛型型變,則需要 <? extends T> 與 <? super T> 通配符,這是一種使用處型變的方法。使用 <? extends T> 通配符意味著該類是生產(chǎn)者,只能調(diào)用 get(): T 之類的方法。而使用 <? super T> 通配符意味著該類是消費(fèi)者,只能調(diào)用 set(T t)、add(T t) 之類的方法。

2、Kotlin 泛型其實(shí)默認(rèn)也是不型變的,只不過(guò)使用 out 和 in 關(guān)鍵字在類聲明處型變,可以達(dá)到在使用處看起來(lái)像直接型變的效果。但是這樣會(huì)限制類在聲明時(shí)只能要么作為生產(chǎn)者,要么作為消費(fèi)者。

使用類型投影可以避免類在聲明時(shí)被限制,但是在使用時(shí)要使用 out 和 in 關(guān)鍵字指明這個(gè)時(shí)刻類所充當(dāng)?shù)慕巧窍M(fèi)者還是生產(chǎn)者。類型投影也是一種使用處型變的方法。

看完了這篇文章,相信你對(duì)Java中kotlin的協(xié)變與逆變是什么有了一定的了解,想了解更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司行業(yè)資訊頻道,感謝各位的閱讀!

當(dāng)前名稱:Java中kotlin的協(xié)變與逆變是什么-創(chuàng)新互聯(lián)
轉(zhuǎn)載來(lái)于:http://m.rwnh.cn/article40/ddodeo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站、外貿(mào)建站、搜索引擎優(yōu)化、App開(kāi)發(fā)、電子商務(wù)、網(wǎng)站設(shè)計(jì)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

微信小程序開(kāi)發(fā)
甘南县| 黎川县| 汉川市| 张家口市| 积石山| 桓台县| 琼中| 比如县| 银川市| 临江市| 桃园市| 德兴市| 萨嘎县| 兴义市| 乐至县| 河北区| 阿克| 荣成市| 平湖市| 多伦县| 长沙县| 禹州市| 新宁县| 余干县| 镇江市| 汝城县| 富平县| 深州市| 班戈县| 荃湾区| 田林县| 靖安县| 高陵县| 剑川县| 竹北市| 黑山县| 胶南市| 临夏市| 屏东县| 灵宝市| 通辽市|