一、泛型
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