1. 基本用法
class Box<T> {
private var element: T? = null
fun add(element: T) {
this.element = element
}
fun get(): T? = element
}
2. 型變
型變包括 協變、逆變、不變 三種:
- 協變:泛型類型與實參的繼承關系相同
- 逆變:泛型類型與實參的繼承關系相反
- 不變:泛型類型與實參類型相同
Java的型變
首先明確一點,Java不直接支持型變。通俗地講,雖然Integer是Number的子類,但是在Java中,List<Integer>和List<Number>是沒有關系的(List<Integer>不是List<Number>的子類)所以List<Integer>不能直接賦值給List<Number>。
如果Java支持型變,則會出現以下情況:
// error:以下代碼實際上會編譯報錯
List<Number> numList = new ArrayList<Integer>();
// 假設以上代碼能夠編譯通過,以下代碼就會在運行時引發異常
// numList實際上操作的集合元素必須是Integer類型
numList.add("haha"); // ClassCastException
Java采用通配符的方式來處理型變的需要(泛型通配符)
public interface Collection<E> extends Iterable<E> {
// Java泛型通配符上限(協變),<? extends E> 可用于接收 E 或 E 的子類型
boolean addAll(Collection<? extends E> c);
// Java泛型通配符下限(逆變),<? super E> 可用于接收 E 或 E 的父類型
boolean removeIf(Predicate<? super E> filter);
}
以上是泛型通配符在Collection接口源碼中的使用
- 通配符上限
public static void test(List<? extends Number> list) {
/*
* 對于“通配符上限”語法而言,從該集合中取出元素是安全的。
* 集合中的元素是Number或其子類型,但是不能往該集合中存入新的元素,
* 因為無法預測該集合元素的實際類型是Integer還是Double,又或者是Number的其他子類型
*/
for (Number num : list) {
System.out.println(num);
}
// list.add() // 不能添加元素,因為不能確定元素的類型
}
- 通配符下限
public static void test(List<? super Integer> list) {
/*
* 對于"通配符下限"語法而言,將對象傳給泛型對象是安全的。
* 該集合中的元素是Integer或其父類型,但是從該集合中取出元素是不安全,
* 因為無法預測該集合元素的實際類型是Number還是Object
*/
list.add(666);
// 以下代碼編譯不通過,因為在取出元素時無法確認元素的類型,可能是Number類型,也可能是Object類型
// Number number = list.get(0);
}
泛型的規律
- 通配符上限(泛型協變)意味著從中取出<font color=red>(out)</font>對象是安全的,但傳入對象是不安全的
- 通配符下限(泛型逆變)意味著向其中傳入<font color=red>(in)</font>對象是安全的,但取出對象是不安全的
Kotlin的型變
Kotlin處理泛型型變的規則就是根據泛型的規律而設計:
- 如果泛型只需要出現在方法的返回值申明中(不出現在形參的聲明中),那么該方法就只是取出泛型對象,因此該方法就支持泛型協變(相當于通配符上限);如果一個類的所有方法都支持泛型協變,那么該類的泛型參數可使用out修飾。
- 如果泛型只需要出現在方法的形參聲明中(不出現在返回值聲明中),那么該方法就只是傳入泛型對象,因此該方法就支持泛型逆變(相當于通配符下限);如果一個類的所有方法都支持泛型逆變,那么該類的泛型參數可使用in修飾。
聲明處型變
- 如果一個類的所有方法都支持泛型協變,那么該類的泛型參數可使用out修飾
class Box<out T> {
private var element: T? = null
fun get(): T? = element
}
fun main(args: Array<String>) {
// 由于泛型使用out修飾,所以 Box<Int> 對象可以直接賦值給 Box<Number>
var box: Box<Number> = Box<Int>()
}
- 如果一個類的所有方法都支持泛型逆變,那么該類的泛型參數可使用in修飾
class Box<in T> {
private var element: T? = null
fun put(e: T) {
this.element = element
}
}
fun main(args: Array<String>) {
// 由于泛型使用in修飾,所以 Box<Any> 對象可以直接賦值給 Box<Number>
var box: Box<Number> = Box<Any>()
}
聲明處型變的限制:</br>如果一個類中有的方法使用泛型聲明返回值類型,有的方法使用泛型聲明形參類型,那么該類就不能使用聲明處型變。
使用處型變
class Box<T> {
private var element: T? = null
fun put(element: T) {
this.element = element
}
fun get(): T? = element
}
fun main(args: Array<String>) {
// 使用 out 修飾泛型
var outBox: Box<out Number> = Box<Int>()
// 不能調用put方法存入數據(編譯報錯)
// outBox.put(18)
val num: Number? = outBox.get()
// 使用 in 修飾泛型
var inBox: Box<in Int> = Box<Number>()
inBox.put(18)
// 不能確定返回值的類型,只能使用Any類型接收
val e: Any? = inBox.get()
}
@UnsafeVariance注解
對于協變的類型,通常是不允許將泛型類型作為方法參數的類型,但是在某些情況下,我們需要在協變的情況下將泛型作為方法的參數類型,那么我們可以使用 @UnsafeVariance 注解來修飾泛型。
class Box<out T> {
private var element: T? = null
// 如果開發者自己可以保證類型安全,那么可以使用@UnsafeVariance注解讓編譯器不報錯
fun put(element: @UnsafeVariance T) {
this.element = element
}
fun get(): T? = element
}
星投影
在Java中,當我們不確定泛型的具體類型是,可以使用 ? 來代替具體的泛型,比如
List<?> list = new ArrayList<String>();
在Kotlin也有類似的語法,可以使用 * 來指代相應的泛型映射
// 星投影
val list: List<*> = ArrayList<String>()
以下是官方對于星投影語法的解釋:
- <p>對于 Foo <out T>,其中 T 是一個具有上界 TUpper 的協變類型參數,Foo <> 等價于 Foo <out TUpper>。 這意味著當 T 未知時,你可以安全地從 Foo <> 讀取 TUpper 的值。</p>
- <p>對于 Foo <in T>,其中 T 是一個逆變類型參數,Foo <> 等價于 Foo <in Nothing>。 這意味著當 T 未知時,沒有什么可以以安全的方式寫入 Foo <>。</p>
- <p>對于 Foo <T>,其中 T 是一個具有上界 TUpper 的不型變類型參數,Foo<*> 對于讀取值時等價于 Foo<out TUpper> 而對于寫值時等價于 Foo<in Nothing>。</p>
簡而言之,星投影就是:
- 當 * 接收可協變的泛型參數 ( out T ) 時,* 映射的類型為 Any?
class Box<out T> {
private var element: T? = null
fun get(): T? = element
}
fun main(args: Array<String>) {
val intBox: Box<Int> = Box()
val numBox: Box<Number> = intBox
val num: Number? = numBox.get()
// 星投影,這里的 <*> 相當于 <out Int>,元素類型映射為 Any?
val box: Box<*> = intBox
val element: Any? = box.get()
}
- 當 * 接收可逆變的泛型參數 ( in T ) 時,* 映射的類型為 Nothing
class Box<in T> {
private var element: T? = null
fun put(element: T) {
this.element = element
}
}
fun main(args: Array<String>) {
// 星投影,這里的 <*> 相當于 <in Number>,元素類型映射為 Nothing
val box: Box<*> = Box<Number>()
box.put(/*element:Nothing*/) // 沒有值可以傳入
}
- 當 * 接收不變的泛型參數 ( T ) 時,* 對于讀取值類型映射為 Any?,而寫值時類型映射為 Nothing
lass Box<T> {
private var element: T? = null
fun put(element: T) {
this.element = element
}
fun get(): T? = element
}
fun main(args: Array<String>) {
// 星投影
val box: Box<*> = Box<Number>()
// 這里的 <*> 在賦值時相當于 <in Int>,元素類型映射為 Nothing
box.put(/*element:Nothing*/) // 沒有值可以傳入
// 這里的 <*> 在取值時相當于 <out Int>,元素類型映射為 Any?
val element: Any? = box.get()
}
3. 泛型函數
泛型函數就是在函數聲明時定義一個或多個泛型,泛型的聲明必須在 fun 與函數名之間
fun <T> test(a: T) {
println(a)
}
fun main(args: Array<String>) {
test<Int>(1) // <Int>可省略,類型通過參數自動推斷
test(2)
}
泛型函數也可以用于擴展函數
fun <T> T.test(): String {
return "test(): ${this.toString()}"
}
fun main(args: Array<String>) {
val num = 666
// 顯示指定泛型為 Int 類型,<Int>可省略
println(num.test<Int>())
// 不顯示指定泛型的類型,編譯器自動推斷出泛型為 String 類型
val str = "haha"
println(str.test())
}
4. 具體化類型參數
Kotlin允許在內聯函數中使用 reified 修飾泛型參數,這樣就可以將該泛型參數變成一個具體化的類型參數。具體化類型參數后就可以在函數中將泛型當做一個普通類型來使用,比如可以使用 is、as 運算符。
比如,我們需要在list中找到指定類型的元素,原先的寫法如下
fun <T> findData(list: List<*>, clazz: Class<T>): T? {
for (e in list) {
if (clazz.isInstance(e)) {
@Suppress("UNCHECKED_CAST")
return e as T
}
}
return null
}
fun main(args: Array<String>) {
val list = listOf(1, 6.6f, "haha")
println(findData(list, String::class.java)) // haha
println(findData(list, Float::class.javaObjectType)) // 6.6
}
使用具體化類型參數之后,代碼如下:
// 很明顯,代碼變得更簡潔了
inline fun <reified T> findData(list: List<*>): T? {
for (e in list) {
if (e is T) {
return e
}
}
return null
}
fun main(args: Array<String>) {
val list = listOf(1, 6.6f, "haha")
println(findData<String>(list))
println(findData<Float>(list))
}
使用reified修飾的泛型參數,還可以對其使用反射
inline fun <reified T> test() {
println(T::class.java)
}
fun main(args: Array<String>) {
test<String>() // class java.lang.String
test<Double>() // class java.lang.Double
}
5. 泛型邊界(上界)
java中可以使用 extends 指定泛型的上界
class Box<T extends Number> {
// ...
}
Koltin的實現:
class Box<T : Number> {
// ...
}
如果需要指定多個邊界,需要使用where子句(只能有一個父類上界,可以有多個接口上界)
class Box<T> where T : Number, T : Comparable<T> {
// ...
}
PS:在Kotlin中,如果不指定邊界,則默認邊界是 Any#