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());
以上分別用 Integer
和 String
作為傳入的類型參數(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)
以上分別用 Int
和 String
作為傳入的類型參數(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é)