亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

目錄
前言
普通賦值
函數(shù)參數(shù)的賦值
函數(shù)返回值的賦值
Java 中的協(xié)變與逆變
Java 中泛型如何實現(xiàn)協(xié)變與逆變
<? extends T> 與 <? super T>
PECS
kotlin 中的協(xié)變與逆變
聲明處型變
類型投影
為什么要這么設(shè)計?
總結(jié)
首頁 Java Java基礎(chǔ) 從Java說起 kotlin 的協(xié)變與逆變

從Java說起 kotlin 的協(xié)變與逆變

Oct 13, 2020 am 11:14 AM
java kotlin

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

從Java說起 kotlin 的協(xié)變與逆變

前言

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

普通賦值

在 Java 中,常見的賦值語句如下:

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

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

除了上述最常見的賦值語句,還有兩種其他的賦值語句:

函數(shù)參數(shù)的賦值

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

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

函數(shù)返回值的賦值

public A fun() {
    B b = new B();    return b;
} 
復(fù)制代碼

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

比如之前舉的例子

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

如果使用上界通配符,

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

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

同樣,下界通配符 可以實現(xiàn)逆變,如:

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

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

從上面可以看出, 中的上界是 T,也就是說 所泛指的類型都是 T 的子類或 T 本身,所以 T 大于 。 中的下界是 T,也就是說 所泛指的類型都是 T 的父類或 T 本身,所以 大于 T。

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

PECS

PECS 準則即 Producer Extends Consumer Super,生產(chǎ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)編譯報錯Container<? extends Object> c = new Container<String>(); // (2)編譯通過c.set("sss"); // (3)編譯報錯Object o = c.get();// (4)編譯通過復(fù)制代碼

代碼 (1),Container<Object> c = new Container<String>(); 編譯報錯,因為泛型是不型變的,所以 Container 并不是 Container 的子類型,所以無法賦值。

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

既然代碼 (2) 通過編譯,那代碼 (3) 為什么會報錯呢?因為代碼 (3) 嘗試把 String 類型賦值給 類型。顯然,編譯器只知道 是 Obejct 的某一個子類型,但是具體是哪一個并不知道,也許并不是 String 類型,所以不能直接將 String 類型賦值給它。

從上面可以看出,對于使用了 的類型,是不能寫入元素的,不然就會像代碼 (3) 處一樣編譯報錯。

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

消費者同理,代碼如下:

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

代碼 (1) 編譯報錯,因為泛型不支持逆變。而且就算不懂泛型,這個代碼的形式一眼看起來也是錯的。

代碼 (2) 編譯通過,因為加了 通配符后,泛型逆變。

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

代碼 (4) 編譯報錯,因為它嘗試把 賦值給 String,而 大于 String,所以不能賦值。事實上,編譯器完全不知道該用什么類型去接受 c.get() 的返回值,因為在編譯器眼里 是一個泛指的類型,所有 String 的父類和 String 本身都有可能。

同樣從上面代碼可以看出,對于使用了 的類型,是不能讀取元素的,不然就會像代碼 (4) 處一樣編譯報錯。但是可以寫入元素,比如代碼 (3)。該類型只能寫入元素,這就是所謂的“消費者”,即只能寫入元素的就是消費者,消費者就使用 通配符。

綜上,這就是 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ù)制代碼

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

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

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

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

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

但是這是毫無意義的,使用通配符只是把類型變得更復(fù)雜,并沒有帶來額外的價值,因為能調(diào)用的方法還是只有生產(chǎn)者方法。但 Java 編譯器只認死理。

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

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

比如:

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

val c: Container<Any> = Container<String>()// (2)編譯通過,因為 T 是一個 out-參數(shù)復(fù)制代碼

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

同樣的,對于消費者來說,

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-參數(shù)復(fù)制代碼

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

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

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

類型投影

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

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

考慮這個函數(shù):

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

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

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

image-20201011204330187.png

這樣使用的話,編譯器報錯,因為我們把兩個不一樣的類型做了賦值。用 kotlin 官方文檔的話說,copy 函數(shù)在”干壞事“, 它嘗試一個 Any 類型的值給 from, 而我們用 Int 類型來接收這個值,如果編譯器不報錯,那么運行時將會拋出一個 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) // 不會再報錯了復(fù)制代碼

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

同理,如果 from 的泛型是用 in 來修飾的話,則 from 只能被當(dāng)作消費者使用,它只能調(diào)用 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) //  報錯復(fù)制代碼

image-20201011210124162.png

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

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

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

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

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

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

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

比如下面兩個方法:

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

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

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

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

然而,對于泛型來說,卻有以下問題:

// 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ù)制代碼

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

總結(jié)

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

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

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

相關(guān)免費學(xué)習(xí)推薦:java基礎(chǔ)教程

以上是從Java說起 kotlin 的協(xié)變與逆變的詳細內(nèi)容。更多信息請關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

本站聲明
本文內(nèi)容由網(wǎng)友自發(fā)貢獻,版權(quán)歸原作者所有,本站不承擔(dān)相應(yīng)法律責(zé)任。如您發(fā)現(xiàn)有涉嫌抄襲侵權(quán)的內(nèi)容,請聯(lián)系admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費脫衣服圖片

Undresser.AI Undress

Undresser.AI Undress

人工智能驅(qū)動的應(yīng)用程序,用于創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用于從照片中去除衣服的在線人工智能工具。

Clothoff.io

Clothoff.io

AI脫衣機

Video Face Swap

Video Face Swap

使用我們完全免費的人工智能換臉工具輕松在任何視頻中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的代碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

功能強大的PHP集成開發(fā)環(huán)境

Dreamweaver CS6

Dreamweaver CS6

視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版

SublimeText3 Mac版

神級代碼編輯軟件(SublimeText3)

熱門話題

Laravel 教程
1597
29
PHP教程
1488
72
VSCODE設(shè)置。JSON位置 VSCODE設(shè)置。JSON位置 Aug 01, 2025 am 06:12 AM

settings.json文件位于用戶級或工作區(qū)級路徑,用于自定義VSCode設(shè)置。1.用戶級路徑:Windows為C:\Users\\AppData\Roaming\Code\User\settings.json,macOS為/Users//Library/ApplicationSupport/Code/User/settings.json,Linux為/home//.config/Code/User/settings.json;2.工作區(qū)級路徑:項目根目錄下的.vscode/settings

如何使用JDBC處理Java的交易? 如何使用JDBC處理Java的交易? Aug 02, 2025 pm 12:29 PM

要正確處理JDBC事務(wù),必須先關(guān)閉自動提交模式,再執(zhí)行多個操作,最后根據(jù)結(jié)果提交或回滾;1.調(diào)用conn.setAutoCommit(false)以開始事務(wù);2.執(zhí)行多個SQL操作,如INSERT和UPDATE;3.若所有操作成功則調(diào)用conn.commit(),若發(fā)生異常則調(diào)用conn.rollback()確保數(shù)據(jù)一致性;同時應(yīng)使用try-with-resources管理資源,妥善處理異常并關(guān)閉連接,避免連接泄漏;此外建議使用連接池、設(shè)置保存點實現(xiàn)部分回滾,并保持事務(wù)盡可能短以提升性能。

在Java的掌握依賴注入春季和Guice 在Java的掌握依賴注入春季和Guice Aug 01, 2025 am 05:53 AM

依賴性(di)IsadesignpatternwhereObjectsReceivedenciesenciesExtern上,推廣looseSecouplingAndEaseerTestingThroughConstructor,setter,orfieldInjection.2.springfraMefringframeWorkSannotationsLikeLikeLike@component@component,@component,@service,@autowiredwithjava-service和@autowiredwithjava-ligatiredwithjava-lase-lightike

Python Itertools組合示例 Python Itertools組合示例 Jul 31, 2025 am 09:53 AM

itertools.combinations用于生成從可迭代對象中選取指定數(shù)量元素的所有不重復(fù)組合(順序無關(guān)),其用法包括:1.從列表中選2個元素組合,如('A','B')、('A','C')等,避免重復(fù)順序;2.對字符串取3個字符組合,如"abc"、"abd",適用于子序列生成;3.求兩數(shù)之和等于目標值的組合,如1 5=6,簡化雙重循環(huán)邏輯;組合與排列的區(qū)別在于順序是否重要,combinations視AB與BA為相同,而permutations視為不同;

故障排除常見的java`ofmemoryError`場景'' 故障排除常見的java`ofmemoryError`場景'' Jul 31, 2025 am 09:07 AM

java.lang.OutOfMemoryError:Javaheapspace表示堆內(nèi)存不足,需檢查大對象處理、內(nèi)存泄漏及堆設(shè)置,通過堆轉(zhuǎn)儲分析工具定位并優(yōu)化代碼;2.Metaspace錯誤因類元數(shù)據(jù)過多,常見于動態(tài)類生成或熱部署,應(yīng)限制MaxMetaspaceSize并優(yōu)化類加載;3.Unabletocreatenewnativethread因系統(tǒng)線程資源耗盡,需檢查線程數(shù)限制、使用線程池、調(diào)整棧大小;4.GCoverheadlimitexceeded指GC頻繁但回收少,應(yīng)分析GC日志,優(yōu)化

Python Pytest夾具示例 Python Pytest夾具示例 Jul 31, 2025 am 09:35 AM

fixture是用于為測試提供預(yù)設(shè)環(huán)境或數(shù)據(jù)的函數(shù),1.使用@pytest.fixture裝飾器定義fixture;2.在測試函數(shù)中以參數(shù)形式注入fixture;3.yield之前執(zhí)行setup,之后執(zhí)行teardown;4.通過scope參數(shù)控制作用域,如function、module等;5.將共用fixture放在conftest.py中實現(xiàn)跨文件共享,從而提升測試的可維護性和復(fù)用性。

了解Java虛擬機(JVM)內(nèi)部 了解Java虛擬機(JVM)內(nèi)部 Aug 01, 2025 am 06:31 AM

TheJVMenablesJava’s"writeonce,runanywhere"capabilitybyexecutingbytecodethroughfourmaincomponents:1.TheClassLoaderSubsystemloads,links,andinitializes.classfilesusingbootstrap,extension,andapplicationclassloaders,ensuringsecureandlazyclassloa

如何使用Java的日歷? 如何使用Java的日歷? Aug 02, 2025 am 02:38 AM

使用java.time包中的類替代舊的Date和Calendar類;2.通過LocalDate、LocalDateTime和LocalTime獲取當(dāng)前日期時間;3.使用of()方法創(chuàng)建特定日期時間;4.利用plus/minus方法不可變地增減時間;5.使用ZonedDateTime和ZoneId處理時區(qū);6.通過DateTimeFormatter格式化和解析日期字符串;7.必要時通過Instant與舊日期類型兼容;現(xiàn)代Java中日期處理應(yīng)優(yōu)先使用java.timeAPI,它提供了清晰、不可變且線

See all articles