Java & Groovy & Scala & Kotlin - 27.泛型

Overview

泛型使類型參數(shù)化變得可能。在聲明類或接口時(shí),可以使用自定義的占位符來表示類型,在運(yùn)行時(shí)由傳入的具體類型進(jìn)行替換。泛型的引入讓集合變得更加好用,使很多錯(cuò)誤在編譯時(shí)就能被發(fā)現(xiàn),也省去了一些強(qiáng)制轉(zhuǎn)換的麻煩。

Java 篇

泛型是 Java 1.5才引進(jìn)的特性。沒有泛型的時(shí)候使用一個(gè)持有特定類型的值的類的時(shí)候是非常麻煩的

例:

public class ObjectCapture {
    private Object object;
    public ObjectCapture(Object o) {
        this.object = o;
    }
    public void set(Object object) {
        this.object = object;
    }
    public Object get() {
        return object;
    }
}

使用以上類

ObjectCapture integerObjectCapture = new ObjectCapture(10);
assert 10 == (Integer) integerObjectCapture.get();

沒有泛型的時(shí)候在取數(shù)據(jù)時(shí)必須進(jìn)行強(qiáng)制轉(zhuǎn)換,但是此時(shí)根本無法保證 之前使用的 ObjectCapture 保存的是 Integer 類型的值,如果是其它類型的話,程序就會(huì)直接掛掉,而且這種錯(cuò)誤只有運(yùn)行時(shí)才能發(fā)現(xiàn)。

創(chuàng)建泛型

類型參數(shù)使用 <類型參數(shù)名> 作為類型的占位符。

public class Capture<T> {
    private T t;
    public Capture(T t) {
        this.t = t;
    }
    public void set(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
}

Java 中最常用的占位符為通用的 "T",表示 Key 的 "K", 表示 Value 的 "V" 和表示異常的 "E"。

使用泛型

Capture<Integer> integerCapture = new Capture<>(10);
assert 10 == integerCapture.get();
Capture<String> stringCapture = new Capture<>("Hi");
assert "Hi".equals(stringCapture.get());

以上分別用 IntegerString 作為傳入的類型參數(shù),如果向這兩個(gè)對(duì)象傳入不符合的類型時(shí)編譯器就會(huì)理解報(bào)錯(cuò),此外取數(shù)據(jù)時(shí)也不用進(jìn)行強(qiáng)制轉(zhuǎn)換,比起沒有泛型時(shí)要方便很多。

類型擦除

Java 的泛型是在編譯器層次實(shí)現(xiàn)的,所以運(yùn)行時(shí)有關(guān)泛型的信息都會(huì)被丟失,這被稱作類型擦除。也就是說上節(jié)例子中的 Capture<Integer>Capture<String> 在運(yùn)行時(shí)都是 Capture 類型,沒有任何區(qū)別。

協(xié)變與逆變

如果 Capture<String> 被看做是 Capture<Object> 的子類型,則稱這種特性為協(xié)變。相反情況則稱為逆變。

協(xié)變

在 Java 中,協(xié)變是默認(rèn)支持,所以可以寫出以下例子:

Integer[] integers = new Integer[2];
Object[] objects = integers;

但是這樣會(huì)造成以下的問題

Date[] dates = new Date[2];
Object[] objects2 = dates;
objects2[0] = "str";

這種代碼在編寫時(shí)完全沒有問題,但是運(yùn)行時(shí)會(huì)拋出異常。所以引進(jìn)泛型時(shí)就不支持協(xié)變,所以以下代碼在編譯時(shí)就會(huì)報(bào)錯(cuò)。

List<Date> dateList = new ArrayList<>();
List<Object> objectList = dateList;

逆變

Java 不支持逆變。

類型通配符

由于泛型不支持協(xié)變,所以在使用泛型作為參數(shù)傳遞時(shí)會(huì)非常麻煩。

private static void foo(List<Object> list) {}

以上例子中是無法將 dateList 傳入 foo() 方法中的。解決方法是使用通配符 ?

private static void foo(List<?> list) {}

以上例子中就能正常傳入 dateList 了。

注意:List<?> 和 List 并不是一個(gè)概念

List 是原生類型,表示不對(duì) List 的類型進(jìn)行限制,可以進(jìn)行各種操作,錯(cuò)誤使用在運(yùn)行時(shí)才能發(fā)現(xiàn)。

List<?> 表示 List 中存放的是某種類型的數(shù)據(jù),只是類型本身并不確定。所以無法建立 List<?> 的實(shí)例,也無法向 List 中追加任何數(shù)據(jù)。

類型參數(shù)邊界

上節(jié)說過無法向 List<?> 中追加任何數(shù)據(jù),這一做法會(huì)讓程序變得非常麻煩,解決方法就是使用類型參數(shù)邊界。類型參數(shù)邊界分為上邊界和下邊界。

上邊界用于限定類型參數(shù)一定是某個(gè)類的子類,使用關(guān)鍵字 extends 指定。下邊界用于限定類型參數(shù)一定是某個(gè)類的超類,使用關(guān)鍵字 super 指定。

上邊界無法確定容器中保存的真實(shí)類型,所以無法向其中追加數(shù)據(jù),但是可以獲得邊界類型的數(shù)據(jù)

private static void foo3(List<? extends Num> list) {
    //        list.add(new Num(4));
    Num num = list.get(0);
}

下邊界可以追加邊界類型的數(shù)據(jù),但是獲得數(shù)據(jù)都只能是 Object 類型

private static void foo4(List<? super Num> list) {
    list.add(new Num(4));
    Object object = list.get(0);
}

上下邊界在這里實(shí)際是起到了協(xié)變和逆變的作用,具體可以對(duì)比 Kotlin 的例子。

Groovy 篇

Groovy 中使用的就是 Java 的泛型,所以參考 Java 就行了。但是要注意的是由于 Groovy 的動(dòng)態(tài)特性,所以有些Java 會(huì)報(bào)的編譯錯(cuò)誤在 Groovy 中只有運(yùn)行時(shí)才會(huì)發(fā)現(xiàn)。

例如以下代碼在 Java 中是非法的,在 Groovy 中雖然編譯通過,但運(yùn)行時(shí)會(huì)報(bào)錯(cuò)

List<Date> dateList = new ArrayList<>()
dateList.add(1)
dateList.add(new Date())

Scala 篇

創(chuàng)建泛型

類型參數(shù)使用 [類型參數(shù)名] 作為類型的占位符,而 Java 用的是 <>。

class Capture[A](val a: A) {
}

Scala 中最常用的占位符為 "A"。

使用泛型

val integerCapture = new Capture[Int](10)
val nint10:Int = integerCapture.a
val stringCapture = new Capture[String]("Hi")
val strHi:String = stringCapture.a
println(strHi)

以上分別用 IntString 作為傳入的類型參數(shù),如果向這兩個(gè)對(duì)象傳入不符合的類型時(shí)編譯器就會(huì)理解報(bào)錯(cuò),此外取數(shù)據(jù)時(shí)也不用進(jìn)行強(qiáng)制轉(zhuǎn)換,比起沒有泛型時(shí)要方便很多。

協(xié)變與逆變

如果 Capture<String> 被看做是 Capture<Object> 的子類型,則稱這種特性為協(xié)變。相反情況則稱為逆變。
在 Scala 中,這兩種特性都是默認(rèn)不支持的。

注意,函數(shù)的參數(shù)是逆變的,函數(shù)的返回值是協(xié)變的。

使用協(xié)變

使用協(xié)變需要在類型前加上 +

定義一個(gè)支持協(xié)變的類,協(xié)變類型參數(shù)只能用作輸出,所以可以作為返回值類型但是無法作為入?yún)⒌念愋?/p>

class CovariantHolder[+A](val a: A) {
  def foo(): A = {
    a
  }
}

使用該類

var strCo = new CovariantHolder[String]("a")
var intCo = new CovariantHolder[Int](3)
var anyCo = new CovariantHolder[AnyRef]("b")

//  Wrong!! Int 不是 AnyRef 的子類
// anyCo = intCo
anyCo = strCo

使用逆變

使用逆變需要在類型前加上 -。

定義一個(gè)支持逆變的類,逆變類型參數(shù)只能用作輸入,所以可以作為入?yún)⒌念愋偷菬o法作為返回值類型

class ContravarintHolder[-A]() {
  def foo(p: A): Unit = {
  }
}

使用該類

var strDCo = new ContravarintHolder[String]()
var intDCo = new ContravarintHolder[Int]()
var anyDCo = new ContravarintHolder[AnyRef]()

//  Wrong!! AnyRef 不是 Int 的超類
// strDCo = anyDCo
strDCo = anyDCo

類型通配符

概念與 Java 基本一致,只是 Scala 使用 _ 作為通配符。

def foo2(capture: Capture[_]): Unit = {
}

類型參數(shù)邊界

上邊界用于限定類型參數(shù)一定是某個(gè)類的子類,使用符號(hào) <: 指定。下邊界用于限定類型參數(shù)一定是某個(gè)類的超類,使用符號(hào) >: 指定。

上邊界無法確定容器中保存的真實(shí)類型,所以無法向其中追加數(shù)據(jù),但是可以獲得邊界類型的數(shù)據(jù)

def foo3(list: collection.mutable.MutableList[_ <: Num]): Unit = {
    //    list += new Num(4)
    val num = list.head
    println(num.number)
}

下邊界可以追加邊界類型的數(shù)據(jù),但是獲得數(shù)據(jù)都只能是 Any 類型

def foo4(list: collection.mutable.MutableList[_ >: Num]): Unit = {
    list += new Num(4)
    val num = list.head
    println(num.asInstanceOf[Num].number)
}

最小類型

Scala 中在表示邊界時(shí)可以使用 Nothing 表示最小類型,即該類為所有類型的子類,所有可以寫出以下代碼。

def foo5(capture: Capture[_ >: Nothing]): Unit = {
}

Kotlin 篇

創(chuàng)建泛型

同 Java。

class Capture<T>(val t: T)

使用泛型

val integerCapture = Capture(10)
val nint10 = integerCapture.t
val stringCapture = Capture("Hi")
val str = stringCapture.t

協(xié)變與逆變

在 Kotlin 中,這兩種特性都是默認(rèn)不支持的。

注意,函數(shù)的參數(shù)是逆變的,函數(shù)的返回值是協(xié)變的。

使用協(xié)變

使用協(xié)變需要在類型前加上 out,相比較 Scala 使用的 + 可能更能讓人理解。

定義一個(gè)支持協(xié)變的類,協(xié)變類型參數(shù)只能用作輸出,所以可以作為返回值類型但是無法作為入?yún)⒌念愋?/p>

class CovariantHolder<out A>(val a: A) {
    fun foo(): A {
        return a
    }
}

使用該類

var strCo: CovariantHolder<String> = CovariantHolder("a")
var anyCo: CovariantHolder<Any> = CovariantHolder<Any>("b")
anyCo = strCo

使用逆變

使用逆變需要在類型前加上 in。

定義一個(gè)支持逆變的類,逆變類型參數(shù)只能用作輸入,所以可以作為入?yún)⒌念愋偷菬o法作為返回值的類型

class ContravarintHolder<in A>(a: A) {
    fun foo(a: A) {
    }
}

使用該類

var strDCo = ContravarintHolder("a")
var anyDCo = ContravarintHolder<Any>("b")
strDCo = anyDCo

類型通配符

Kotlin 使用 * 作為通配符,而 Java 是 ?,Scala 是 _。

fun foo2(capture: Capture<*>) {
}

類型參數(shù)邊界

Kotlin 并沒有上下邊界這種說法。但是可以通過在方法上使用協(xié)變和逆變來達(dá)到同樣的效果。

使用協(xié)變參數(shù)達(dá)到上邊界的作用,這里的 out 很形象地表示了協(xié)變參數(shù)只能用于輸出

fun foo3(list: MutableList<out Num>) {
    val num: Num = list.get(0)
    println(num)
}

使用逆變參數(shù)達(dá)到下邊界的作用,這里的 in 很形象地表示了逆變參數(shù)只能用于輸入

fun foo4(list: MutableList<in Num>) {
    list.add(Num(4))
    val num: Any? = list.get(0)
    println(num)
}

Summary

  • Java 和 Groovy 的用法完全一致,都只支持逆變
  • Scala 和 Kotlin 支持逆變和協(xié)變,但是都需要顯示指定
  • Java 使用 ? 作為通配符,Scala 使用 _,Kotlin 使用 *

文章源碼見 https://github.com/SidneyXu/JGSK 倉庫的 _27_generics 小節(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,701評(píng)論 9 118
  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符,讓我看了幾遍《Java...
    珞澤珈群閱讀 8,129評(píng)論 12 51
  • object 變量可指向任何類的實(shí)例,這讓你能夠創(chuàng)建可對(duì)任何數(shù)據(jù)類型進(jìn)程處理的類。然而,這種方法存在幾個(gè)嚴(yán)重的問題...
    CarlDonitz閱讀 1,020評(píng)論 0 5
  • 本文大量參考Thinking in java(解析,填充)。 定義:多態(tài)算是一種泛化機(jī)制,解決了一部分可以應(yīng)用于多...
    谷歌清潔工閱讀 522評(píng)論 0 2
  • 一個(gè)女人在打電話 她的太陽鏡下藏了什么? 傷心的眼淚? 失望的淚水? 兇狠的目光? 她的包里又藏了什么? 手槍? ...
    上官血櫻閱讀 290評(píng)論 0 1

友情鏈接更多精彩內(nèi)容