Overview
泛型使類型參數化變得可能。在聲明類或接口時,可以使用自定義的占位符來表示類型,在運行時由傳入的具體類型進行替換。泛型的引入讓集合變得更加好用,使很多錯誤在編譯時就能被發現,也省去了一些強制轉換的麻煩。
Java 篇
泛型是 Java 1.5才引進的特性。沒有泛型的時候使用一個持有特定類型的值的類的時候是非常麻煩的
例:
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();
沒有泛型的時候在取數據時必須進行強制轉換,但是此時根本無法保證 之前使用的 ObjectCapture
保存的是 Integer
類型的值,如果是其它類型的話,程序就會直接掛掉,而且這種錯誤只有運行時才能發現。
創建泛型
類型參數使用 <類型參數名>
作為類型的占位符。
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
作為傳入的類型參數,如果向這兩個對象傳入不符合的類型時編譯器就會理解報錯,此外取數據時也不用進行強制轉換,比起沒有泛型時要方便很多。
類型擦除
Java 的泛型是在編譯器層次實現的,所以運行時有關泛型的信息都會被丟失,這被稱作類型擦除。也就是說上節例子中的 Capture<Integer>
和 Capture<String>
在運行時都是 Capture
類型,沒有任何區別。
協變與逆變
如果 Capture<String> 被看做是 Capture<Object> 的子類型,則稱這種特性為協變。相反情況則稱為逆變。
協變
在 Java 中,協變是默認支持,所以可以寫出以下例子:
Integer[] integers = new Integer[2];
Object[] objects = integers;
但是這樣會造成以下的問題
Date[] dates = new Date[2];
Object[] objects2 = dates;
objects2[0] = "str";
這種代碼在編寫時完全沒有問題,但是運行時會拋出異常。所以引進泛型時就不支持協變,所以以下代碼在編譯時就會報錯。
List<Date> dateList = new ArrayList<>();
List<Object> objectList = dateList;
逆變
Java 不支持逆變。
類型通配符
由于泛型不支持協變,所以在使用泛型作為參數傳遞時會非常麻煩。
private static void foo(List<Object> list) {}
以上例子中是無法將 dateList
傳入 foo()
方法中的。解決方法是使用通配符 ?
。
private static void foo(List<?> list) {}
以上例子中就能正常傳入 dateList
了。
注意:List<?> 和 List 并不是一個概念
List 是原生類型,表示不對 List 的類型進行限制,可以進行各種操作,錯誤使用在運行時才能發現。
List<?> 表示 List 中存放的是某種類型的數據,只是類型本身并不確定。所以無法建立 List<?> 的實例,也無法向 List 中追加任何數據。
類型參數邊界
上節說過無法向 List<?> 中追加任何數據,這一做法會讓程序變得非常麻煩,解決方法就是使用類型參數邊界。類型參數邊界分為上邊界和下邊界。
上邊界用于限定類型參數一定是某個類的子類,使用關鍵字 extends
指定。下邊界用于限定類型參數一定是某個類的超類,使用關鍵字 super
指定。
上邊界無法確定容器中保存的真實類型,所以無法向其中追加數據,但是可以獲得邊界類型的數據
private static void foo3(List<? extends Num> list) {
// list.add(new Num(4));
Num num = list.get(0);
}
下邊界可以追加邊界類型的數據,但是獲得數據都只能是 Object 類型
private static void foo4(List<? super Num> list) {
list.add(new Num(4));
Object object = list.get(0);
}
上下邊界在這里實際是起到了協變和逆變的作用,具體可以對比 Kotlin 的例子。
Groovy 篇
Groovy 中使用的就是 Java 的泛型,所以參考 Java 就行了。但是要注意的是由于 Groovy 的動態特性,所以有些Java 會報的編譯錯誤在 Groovy 中只有運行時才會發現。
例如以下代碼在 Java 中是非法的,在 Groovy 中雖然編譯通過,但運行時會報錯
List<Date> dateList = new ArrayList<>()
dateList.add(1)
dateList.add(new Date())
Scala 篇
創建泛型
類型參數使用 [類型參數名]
作為類型的占位符,而 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
作為傳入的類型參數,如果向這兩個對象傳入不符合的類型時編譯器就會理解報錯,此外取數據時也不用進行強制轉換,比起沒有泛型時要方便很多。
協變與逆變
如果 Capture<String> 被看做是 Capture<Object> 的子類型,則稱這種特性為協變。相反情況則稱為逆變。
在 Scala 中,這兩種特性都是默認不支持的。
注意,函數的參數是逆變的,函數的返回值是協變的。
使用協變
使用協變需要在類型前加上 +
。
定義一個支持協變的類,協變類型參數只能用作輸出,所以可以作為返回值類型但是無法作為入參的類型
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
使用逆變
使用逆變需要在類型前加上 -
。
定義一個支持逆變的類,逆變類型參數只能用作輸入,所以可以作為入參的類型但是無法作為返回值類型
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 = {
}
類型參數邊界
上邊界用于限定類型參數一定是某個類的子類,使用符號 <:
指定。下邊界用于限定類型參數一定是某個類的超類,使用符號 >:
指定。
上邊界無法確定容器中保存的真實類型,所以無法向其中追加數據,但是可以獲得邊界類型的數據
def foo3(list: collection.mutable.MutableList[_ <: Num]): Unit = {
// list += new Num(4)
val num = list.head
println(num.number)
}
下邊界可以追加邊界類型的數據,但是獲得數據都只能是 Any 類型
def foo4(list: collection.mutable.MutableList[_ >: Num]): Unit = {
list += new Num(4)
val num = list.head
println(num.asInstanceOf[Num].number)
}
最小類型
Scala 中在表示邊界時可以使用 Nothing
表示最小類型,即該類為所有類型的子類,所有可以寫出以下代碼。
def foo5(capture: Capture[_ >: Nothing]): Unit = {
}
Kotlin 篇
創建泛型
同 Java。
class Capture<T>(val t: T)
使用泛型
val integerCapture = Capture(10)
val nint10 = integerCapture.t
val stringCapture = Capture("Hi")
val str = stringCapture.t
協變與逆變
在 Kotlin 中,這兩種特性都是默認不支持的。
注意,函數的參數是逆變的,函數的返回值是協變的。
使用協變
使用協變需要在類型前加上 out
,相比較 Scala 使用的 +
可能更能讓人理解。
定義一個支持協變的類,協變類型參數只能用作輸出,所以可以作為返回值類型但是無法作為入參的類型
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
。
定義一個支持逆變的類,逆變類型參數只能用作輸入,所以可以作為入參的類型但是無法作為返回值的類型
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<*>) {
}
類型參數邊界
Kotlin 并沒有上下邊界這種說法。但是可以通過在方法上使用協變和逆變來達到同樣的效果。
使用協變參數達到上邊界的作用,這里的 out
很形象地表示了協變參數只能用于輸出
fun foo3(list: MutableList<out Num>) {
val num: Num = list.get(0)
println(num)
}
使用逆變參數達到下邊界的作用,這里的 in
很形象地表示了逆變參數只能用于輸入
fun foo4(list: MutableList<in Num>) {
list.add(Num(4))
val num: Any? = list.get(0)
println(num)
}
Summary
- Java 和 Groovy 的用法完全一致,都只支持逆變
- Scala 和 Kotlin 支持逆變和協變,但是都需要顯示指定
- Java 使用
?
作為通配符,Scala 使用_
,Kotlin 使用*
文章源碼見 https://github.com/SidneyXu/JGSK 倉庫的 _27_generics
小節