1. 基本用法
class Box {
private var element: T? = null
fun add(element: T) {
this.element = element
}
fun get(): T? = element
}
2. 型变
型变包括 协变、逆变、不变 三种:
- 协变:泛型类型与实参的继承关系相同
- 逆变:泛型类型与实参的继承关系相反
- 不变:泛型类型与实参类型相同
Java的型变
首先明确一点,Java不直接支持型变。通俗地讲,虽然Integer是Number的子类,但是在Java中,List
如果Java支持型变,则会出现以下情况:
// error:以下代码实际上会编译报错
List numList = new ArrayList();
// 假设以上代码能够编译通过,以下代码就会在运行时引发异常
// numList实际上操作的集合元素必须是Integer类型
numList.add("haha"); // ClassCastException
Java采用通配符的方式来处理型变的需要(泛型通配符)
public interface Collection extends Iterable {
// 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);
}
泛型的规律
- 通配符上限(泛型协变)意味着从中取出(out)对象是安全的,但传入对象是不安全的
- 通配符下限(泛型逆变)意味着向其中传入(in)对象是安全的,但取出对象是不安全的
Kotlin的型变
Kotlin处理泛型型变的规则就是根据泛型的规律而设计:
- 如果泛型只需要出现在方法的返回值申明中(不出现在形参的声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限);如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰。
- 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限);如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰。
声明处型变
- 如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用out修饰
class Box {
private var element: T? = null
fun get(): T? = element
}
fun main(args: Array) {
// 由于泛型使用out修饰,所以 Box 对象可以直接赋值给 Box
var box: Box = Box()
}
- 如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用in修饰
class Box {
private var element: T? = null
fun put(e: T) {
this.element = element
}
}
fun main(args: Array) {
// 由于泛型使用in修饰,所以 Box 对象可以直接赋值给 Box
var box: Box = Box()
}
声明处型变的限制:如果一个类中有的方法使用泛型声明返回值类型,有的方法使用泛型声明形参类型,那么该类就不能使用声明处型变。
使用处型变
class Box {
private var element: T? = null
fun put(element: T) {
this.element = element
}
fun get(): T? = element
}
fun main(args: Array) {
// 使用 out 修饰泛型
var outBox: Box = Box()
// 不能调用put方法存入数据(编译报错)
// outBox.put(18)
val num: Number? = outBox.get()
// 使用 in 修饰泛型
var inBox: Box = Box()
inBox.put(18)
// 不能确定返回值的类型,只能使用Any类型接收
val e: Any? = inBox.get()
}
@UnsafeVariance注解
对于协变的类型,通常是不允许将泛型类型作为方法参数的类型,但是在某些情况下,我们需要在协变的情况下将泛型作为方法的参数类型,那么我们可以使用 @UnsafeVariance 注解来修饰泛型。
class Box {
private var element: T? = null
// 如果开发者自己可以保证类型安全,那么可以使用@UnsafeVariance注解让编译器不报错
fun put(element: @UnsafeVariance T) {
this.element = element
}
fun get(): T? = element
}
星投影
在Java中,当我们不确定泛型的具体类型是,可以使用 ? 来代替具体的泛型,比如
List> list = new ArrayList();
在Kotlin也有类似的语法,可以使用 * 来指代相应的泛型映射
// 星投影
val list: List<*> = ArrayList()
以下是官方对于星投影语法的解释:
对于 Foo
,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,你可以安全地从 Foo < > 读取 TUpper 的值。对于 Foo
,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo < >。对于 Foo
,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo 而对于写值时等价于 Foo 。
简而言之,星投影就是:
- 当 * 接收可协变的泛型参数 ( out T ) 时,* 映射的类型为 Any?
class Box {
private var element: T? = null
fun get(): T? = element
}
fun main(args: Array) {
val intBox: Box = Box()
val numBox: Box = intBox
val num: Number? = numBox.get()
// 星投影,这里的 <*> 相当于 ,元素类型映射为 Any?
val box: Box<*> = intBox
val element: Any? = box.get()
}
- 当 * 接收可逆变的泛型参数 ( in T ) 时,* 映射的类型为 Nothing
class Box {
private var element: T? = null
fun put(element: T) {
this.element = element
}
}
fun main(args: Array) {
// 星投影,这里的 <*> 相当于 ,元素类型映射为 Nothing
val box: Box<*> = Box()
box.put(/*element:Nothing*/) // 没有值可以传入
}
- 当 * 接收不变的泛型参数 ( T ) 时,* 对于读取值类型映射为 Any?,而写值时类型映射为 Nothing
lass Box {
private var element: T? = null
fun put(element: T) {
this.element = element
}
fun get(): T? = element
}
fun main(args: Array) {
// 星投影
val box: Box<*> = Box()
// 这里的 <*> 在赋值时相当于 ,元素类型映射为 Nothing
box.put(/*element:Nothing*/) // 没有值可以传入
// 这里的 <*> 在取值时相当于 ,元素类型映射为 Any?
val element: Any? = box.get()
}
3. 泛型函数
泛型函数就是在函数声明时定义一个或多个泛型,泛型的声明必须在 fun 与函数名之间
fun test(a: T) {
println(a)
}
fun main(args: Array) {
test(1) // 可省略,类型通过参数自动推断
test(2)
}
泛型函数也可以用于扩展函数
fun T.test(): String {
return "test(): ${this.toString()}"
}
fun main(args: Array) {
val num = 666
// 显示指定泛型为 Int 类型,可省略
println(num.test())
// 不显示指定泛型的类型,编译器自动推断出泛型为 String 类型
val str = "haha"
println(str.test())
}
4. 具体化类型参数
Kotlin允许在内联函数中使用 reified 修饰泛型参数,这样就可以将该泛型参数变成一个具体化的类型参数。具体化类型参数后就可以在函数中将泛型当做一个普通类型来使用,比如可以使用 is、as 运算符。
比如,我们需要在list中找到指定类型的元素,原先的写法如下
fun findData(list: List<*>, clazz: Class): T? {
for (e in list) {
if (clazz.isInstance(e)) {
@Suppress("UNCHECKED_CAST")
return e as T
}
}
return null
}
fun main(args: Array) {
val list = listOf(1, 6.6f, "haha")
println(findData(list, String::class.java)) // haha
println(findData(list, Float::class.javaObjectType)) // 6.6
}
使用具体化类型参数之后,代码如下:
// 很明显,代码变得更简洁了
inline fun findData(list: List<*>): T? {
for (e in list) {
if (e is T) {
return e
}
}
return null
}
fun main(args: Array) {
val list = listOf(1, 6.6f, "haha")
println(findData(list))
println(findData(list))
}
使用reified修饰的泛型参数,还可以对其使用反射
inline fun test() {
println(T::class.java)
}
fun main(args: Array) {
test() // class java.lang.String
test() // class java.lang.Double
}
5. 泛型边界(上界)
java中可以使用 extends 指定泛型的上界
class Box {
// ...
}
Koltin的实现:
class Box {
// ...
}
如果需要指定多个边界,需要使用where子句(只能有一个父类上界,可以有多个接口上界)
class Box where T : Number, T : Comparable {
// ...
}
PS:在Kotlin中,如果不指定边界,则默认边界是 Any#