一、泛型
1.1 含义
参数化类型,用尖括号这种方式表示,如<T>、<E>、<?>等。比如:方法的参数一般指定具体类型,如果把参数的类型也参数化,那这就是泛型本尊了。
|
interface List<out E> : Collection<E> {
override fun contains(element: @UnsafeVariance E): Boolean
}
|
总的来说,泛型本质就是参数化类型,这种类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
1.2 好处
让类型更加安全。
- 编译时类型检查。将错误暴露在编译期,不用等到运行时(防止在运行时出现 ClassCastException)。
- 运行时自动类型转换。不用类型强制转换的烦琐操作。
- 更加语义化(比如:List<String>清楚知道存储的是 String 对象)和能写出更加通用化的代码(引入泛型后并未增加代码的冗余性)。
1.3 类型约束
泛型本身就有类型约束的作用,那么这里的类型约束实际上指的是一种缩小范围的约束。
目标:理解3个问题——上界约束是什么?类型是否为空?多个条件约束怎么办?
1.3.1 上界约束
最常见的约束就是与 Java 的 extends 关键字对应的上界约束,如:class FruitPlate<T : Fruit>(val t: T)。
冒号之后指定的类型 Fruit 是上界:只有 Fruit 的子类型可以替代 T。
|
class Demo {
class Noodles(weight: Double) // 面条
open class Fruit(val weight: Double) // 水果
class Apple(weight: Double) : Fruit(weight) // 苹果
class Banana(weight: Double) : Fruit(weight) // 香蕉
class FruitPlate<T : Fruit>(val t: T) // 上界约束
fun demo() {
FruitPlate(Apple(1.6))
FruitPlate(Banana(1.6))
FruitPlate(Noodles(1.6)) // 报错
}
}
|
如果没有声明,默认的上界是 Any?。声明上界后也可以使用 ? 手动指定可空。
|
class Demo {
open class Fruit(val weight: Double) // 水果
class FruitPlate<T : Fruit?>(val t: T) // 上界约束
fun demo() {
FruitPlate(null) // 正确
}
}
|
在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,那么需要一个单独的 where 子句,如:fun <T> cut(t: T) where T : Fruit, T : Ground {}
|
class Demo {
interface Ground // 土地
class Noodles(weight: Double) // 面条
open class Fruit(val weight: Double) // 水果
class Banana(weight: Double) : Fruit(weight) // 香蕉
class Watermelon(weight: Double) : Fruit(weight), Ground // 西瓜:长在土地的水果
fun <T> cut(t: T) where T : Fruit, T : Ground {}
fun demo() {
cut(Watermelon(1.6)) // 所传递的类型 T 必须同时满足 where 子句的所有条件
cut(Noodles(1.6)) // 报错
cut(Banana(1.6)) // 报错
}
}
|
多个上界助记语:where关键字,逗号分隔开。
二、泛型的背后:类型擦除
2.1 泛型与数组对比
2.1.1 Java 中泛型与数组对比
泛型:Java 泛型是不变的(Fruit 是 Apple 的父类,List<Fruit> 不是 List<Apple> 的父类),是类型擦除的,可以看做伪泛型,无法在程序运行时获取到一个对象的具体类型。保证类型安全。
数组:Java 数组是协变的(Fruit 是 Apple 的父类,Fruit[] 是 Apple[] 的父类),在程序运行时可以获取自身的类型。保证类型安全。
|
public class JavaDemo {
Apple[] appleArray = new Apple[10];
Fruit[] fruitArray = appleArray; // 允许
List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = appleList; // 不允许
public static class Apple implements Fruit { }
public interface Fruit { }
}
|
2.1.2 Kotlin 中泛型与数组对比
泛型:kotlin 中的泛型机制和Java一样
数组:kotlin 数组支持泛型,不再协变(Java 数组不支持泛型,支持协变)
|
class Demo {
open class Fruit(val weight: Double) // 水果
class Apple(weight: Double) : Fruit(weight) // 苹果
private val appleArray = arrayOfNulls<Apple>(5)
val anyArray: Array<Fruit?> = appleArray // 不允许
}
|
2.2 小思考
1、Java 为什么使用类型擦除来实现泛型,并且怎么满足泛型应该具有的特性(类型检查、类型自动转换)?
Tips:向后兼容,强制类型转换
2、为什么 Java 中数组不支持泛型?
Tips:不再保证类型安全。数组内的元素必须是“物化”的,因为类型被擦除后,虚拟机建立数组时不知道类型,而虚拟机要求数组建立时必须明确类型和长度。
反证法:假如给数组加上泛型后,将无法满足数组协变的原则,因为在运行时无法知道数组的类型。
3、类型擦除后,Kotlin 如何在运行时知道泛型参数的类型?(其实并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应 class 的常量池中)
Tips:匿名内部类、内联函数 reified