泛型是一种编译时的安全检测机制,他允许在定义类、接口、方法时使用类型参数,在声明时使用具体的类型来进行替换。
泛型的定义
现实生活中,在整理物品时,会把各种各样的物品分类放好,此时便用到了收纳盒。收纳盒很好,他来者不拒,而收纳盒在装下物品之前,他也不知道里面装的内容是什么。
正如List集合一致,它可以装下各种各样的数据类型,如String、Int、Object等。但想要知道集合中的元素是什么类型的,只能在装入元素之后才能得知。
当在对一个空的集合使用add方法加入元素时,加入的元素是一个不确定的类型,此时就可以使用泛型。泛型即“参数化类型”,意为被操作的数据类型(如String、Int、Object等)被指定为一个参数,此时就是“(数据)类型”被“参数化”了。
在代码中,以ArrayList为例:
class ArrayList<E>
此处的E便是泛型数据类型,表示的是某种类型,定义时是一个未知的类型。当创建ArrayList实例时,需要传入具体的类型。
var list1 = arrayListOf<String>("aa", "bb", "cc")
var list2 = arrayListOf<Long>(111, 222, 333)
var list3 = arrayListOf<Int>(1, 2)
上述示例中传入的String、Long、Int都是类型实参,代替了E的泛型数据类型。
class Box<T>(t: T){
var value = t
}
上述示例中T表示泛型的类型参数,要创建Box类的实例,则需要提供具体的类型参数到T的位置。
val box: Box<Int> = Box<Int>(1)
//若类型参数可以被推断,可省略
val box = Box<Int>(1)
由于传递到构造函数的参数是1,是一个Int类型的参数,编译器会自动推断,因此可以省略。
泛型的分类
泛型可以体现在类、接口以及方法中。
泛型类
定义
使用泛型标记的类,被称为泛型类。
泛型类的使用分为两种情况:泛型类被用于实例化&被用于继承。
当泛型类被用于实例化时,需要传递具体的类型实参。
泛型类被用于实例化
val list = ArrayList<String>() //String类型为泛型实参
val map = HashMap<String, Int>() //String、Int类型为泛型实参
val set = HashSet(Long)() //Long类型为泛型实参
泛型类被用于继承
此时需要为泛型形参提供一个具体类型,或者另一个类型的形参。
class ArrayList<E> : AbstractList<E>(), List<E>, java.io.Serializable {
override val size: Int = 0
override fun get(index: Int): E {
TODO("Not yet implemented")
}
}
示例中,ArrayList<E>表示定义了一个泛型类,其中的<E>则是声明了一个泛型实参(又称占位符),具体的实参会在ArrayList被实例化时传入。
代码中的AbstractList<E>(), List<E>两个泛型类在ArrayList<E>泛型类中被使用,他们传递的具体类型暂不确定,但是这两个类传入的具体类型是与ArrayList<E>一致的。
自定义泛型类
除了使用系统提供的泛型类以外,还可以自定义泛型类,格式如下所示:
[访问修饰符]class类名<泛型符号1, 泛型符号2, ……>{
泛型符号1泛型成员1;
泛型符号2泛型成员2;
}
可见泛型声明在类名之后,符号一般使用大写字母(如<E>、<T>、<TYPE>)。一个类可以声明多个泛型,只要使用不同的泛型符号即可。
class Box<T> {
var t: T? = null //可空变量
fun add(t: T): Unit { //Kotlin的Unit相当于Java的Void
this.t = t;
}
fun get(): T? {
return t
}
}
data class Apple(val name: String) //Apple数据类
fun main() {
val box = Box<Apple>()
box.add(Apple("红富士苹果"))
val apple = box.get()
println(apple.toString())
}
示例中,自定义了一个泛型类Box和数据类Apple。
在泛型类Box中,创建了一个类型为T的变量t,T为泛型,传入的是什么类型,t就是什么类型。
接着创建了add方法与get方法,分别用于设置和获取变量t的值。
在main方法中,实例化了Box类和Apple类,接着调用Box类中的add方法,将Apple类的实例传入,最后通过get方法获取变量t的值并打印。
泛型接口
使用泛型标记的接口,被称为泛型接口。泛型接口的使用分为两种情况:
- 泛型接口被实现时可以确定泛型接口对应的实参,直接传递实参即可;
- 泛型接口被实现时不能确定泛型接口对应的实参,需要使用当前类或者接口的泛型形参。
//情形1:确定泛型接口对应的实参,直接传递实参
interface List<String>:Collection<String>{}
//情形2:不确定,使用当前类或者接口的泛型形参
interface List<E>:Collection<E>{}
泛型方法
定义
使用泛型标记的方法,被称为泛型方法。
//String类型为泛型实参,Kotlin自动推断
val list = arrayListOf("a", "b", "c")
//String、Int类型为泛型实参,Kotlin自动推断
val map = hashMapOf("a" to 1, "b" to 2)
//Long类型为泛型实参,Kotlin自动推断
val set = hashSetOf(1L, 2L, 3L)
高阶函数的泛型方法
在高阶函数中自定义了很多泛型方法。
val letters = ('a'..'z').toList()
println(letters.slice<Char>(0..2))//调用泛型方法,显式指定类型实参
//运行结果:[a, b, c]
println(letters.slice(10..13))//调用泛型方法,编译器推断出T是Char
//运行结果:[k, l, m, n]
自定义泛型方法
//自定义泛型方法的格式
修饰符 fun <泛型符号>方法名(方法参数):方法返回值{
...
}
//示例
fun <T> printInfo(content: T) {
when (content) {
is Int -> println("传入的$content,是一个Int类型")
is String -> println("传入的$content,是一个String类型")
else -> println("传入的$content,不是Int也不是String")
}
}
fun main() {
printInfo(10) //运行结果:传入的10,是一个Int类型
printInfo("hello world") //运行结果:传入的hello world,是一个String类型
printInfo(true) //运行结果:传入的true,不是Int也不是String
}
泛型约束
必要性
创建一个泛型List<E>时,E理论上是可以被替换为任意的引用类型,但有时候需要限制。
fun <T : Number> List<T>.sum(): Double? {
var sum: Double? = 0.0
for (i in this.indices) { //遍历传递过来的集合中数据
sum = sum?.plus(this[i].toDouble()) //转化为Double类型再与sum相加
}
return sum
}
fun main() {
var list = arrayListOf(1, 2, 3, 4, 5)
println("求和:${list.sum()}") //运行结果:求和:15.0
}
示例中,创建了一个泛型方法sum,返回值为Double,其中的反省为List<T>,对T的约束为Number类型,也就是说,调用sum方法的集合,里面的元素必须都是Number类型的。
泛型约束<T: 类或接口>
与Java中的<? extends 类或接口>类似,这个约束也可以理解为泛型的上界。
此处举一个Java的例子:上界 <? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。
回到Kotlin时,例如泛型约束<T: BoundingType>,其中BoundingType可以被称为绑定类型。
绑定类型可以是类或者是接口。
若绑定类型是一个类,则T必须是BoundingType的子类;
若绑定类型是一个接口,则T必须是BoundingType的实现类。
调用泛型上界类中的方法
若泛型约束中制定了类型参数的上界,则可以调用定义在上界类中的方法。
fun <T : Number> twice(value: T): Double {
return value.toDouble() * 2
}
fun main() {
//将数字传递到twice中并打印结果
println("4.0的两倍:${twice(4.0f)}") //运行结果:4.0的两倍:8.0
println("4的两倍:${twice(4)}") //运行结果:4的两倍:8.0
}
twice方法中,value变量调用的toDouble方法是定义在Number类中的。由于在泛型约束中已经指定类型参数的上界为Number,因此变量value可以使用定义在上界类Number中的方法。
若上界约束需要多个约束,可以通过where语句来实现。
fun <T> manyConstraints(value: T) where T : CharSequence, T : Appendable {
if (!value.endsWith('.')) {
value.append('.')
}
}
示例中,通过where实现了多个上界约束,每个约束之间用逗号分隔。传递的参数value可以调用第1个约束CharSequence类中的endsWith()方法,同时也可以调用第2个约束Appendable类中的append方法。
泛型约束<T: Any?>与<T: Any>
在泛型约束<T: 类或接口>中,有两种特殊的形式,就是本节的标题。
前者表示的是类型实参是Any的子类,且可以为null,后者与前者的差别在于不能为null。
在Kotlin中,Any类型是任意类型的父类,类似Java中的Object类,因此<T: Any?>等同于<T>。
fun <T : Any?> nullAbleProcessor(value: T) {
value?.hashCode()
}
fun <T : Any> nullDisableProcessor(value: T) {
value?.hashCode()
}
fun main() {
nullAbleProcessor(null)
//nullDisableProcessor(null) 编译不通过
}
若想在示例的nullDisableProcessor方法中传递null,可做如下改动:
//改动前
fun <T : Any> nullDisableProcessor(value: T) {
value?.hashCode()
}
//改动后
fun <T : Any> nullDisableProcessor(value: T?) {
value?.hashCode()
}
子类和子类型
子类与子类型是不同的,子类是继承的概念,若b继承a,b就是a子类。
若需要使用类型a的变量,可以使用类型b的变量来代替,则此时类型b是a的子类型。
子类说明是一个新类继承了父类,而子类型则是强调,新类具有父类一样的行为。
继承与子类型
open class Animal {
fun bathe() {
println("Bathing")
}
}
class Cat : Animal()
fun work(animal: Animal): Unit {
animal.bathe()
}
fun main() {
var cat = Cat()
work(cat)//运行结果:Bathing
}
示例中,创建了两个类,Cat类继承Animal类。调用work方法时,方法中需要传递的参数类型是Animal,但在实际操作中,传递到该方法中的参数类型是Cat类型,此代码可以编译通过,说明Cat类型是Animal类的子类型。
若a类不是b类的子类型,则不可以代替b类做一些事情。
fun output(number: Number): Unit {
println(number)
}
fun main() {
var i = 1
output(i)
var str = "Hello"
output(str)
}
示例中,output方法需要传递的是Number类型的数据。当传递类型为Int的变量i时,传递成功;但当传递类型为String的变量str时,将会提示编译错误(如上图)。出现这些情况是因为,Int类是Number类的子类型,但String不是。
接口与子类型
若b类实现了接口类a,则b类就是接口a的子类型。
fun export(str: CharSequence): Unit {
println(str)
}
fun main() {
val str: String = "Hello Kotlin"
export(str)
}
示例中,export方法传递的参数是CharSequence类型的,但在main方法中传递的变量str是String类型的,此时编译会通过。说明当一个类实现了一个接口,这个类就是这个接口的子类型。
可空类型的子类型
非空类型String是可空类型String?的子类型。
fun print(str: String?): Unit {
println(str)
}
fun main() {
var str1: String = "not null"
var str2: String? = null
print(str1)//运行结果:not null
print(str2)//运行结果:null
}
示例中,print方法需要传递可空参数,但在main方法中调用print方法时,传递了非空和可空参数,此时编译器没有报错,也可以正常运行。
这说明非空类型的String类型是可空类型的String?类型的子类型。
而将两者调换位置时,在main方法里调用print方法的部分将报错。
需要注意的是,虽B是A的子类型,但Xxx<B>不是Xxx<A>的子类型。此处的Xxx表示的是一个类或者接口,例如可以是List接口、PetShop类等。
open class Animal {
fun bathe() {
println("bathing")
}
}
class Cat : Animal() //猫类
class PetShop<T : Animal>(var animal: List<T>) //宠物店类
fun batheAll(petShop: PetShop<Animal>) { //洗澡的方法
for (animal: Animal in petShop.animal) {
animal.bathe()//开洗
}
}
fun main() {
val cat1 = Cat()
val cat2 = Cat()
val animals = listOf<Cat>(cat1, cat2) //将定义的两只猫装入一个集合
val petShop = PetShop<Cat>(animals) //将宠物送到宠物店
batheAll(petShop) //此处编译器报错
}
示例中,Cat类是Animal类的子类,但List<Cat>不是List<Animal>的子类,同时PetShop<Cat>也不是PetShop<Animal>的子类。
在定义petShop变量时,Cat类可以代替Animal类,说明Cat类是其子类,但在最后一行中,batheAll方法需要传递的变量是PetShop<Animal>类型的,而实际传递PetShop<Cat>时出现了报错,提示为“Type mismatch”。
协变与逆变
协变是将父类变成具体子类,协变作为消费者,只能读取不能写入。
逆变是将子类变为具体父类,逆变作为生产者,只能写入不能读取。
协变(out)
在上一小节中,B类是A类的子类型,默认情况下Xxx<B>不是Xxx<A>的子类型,但通过out关键字可以改变这一情况,这样的操作就叫做协变。
open class Animal {
fun bathe() {
println("bathing")
}
}
class Cat : Animal() //猫类
class PetShop<T : Animal>(var animal: List<T>) //宠物店类
fun batheAll(petShop: PetShop<out Animal>) { //洗澡的方法,添加out关键字
for (animal: Animal in petShop.animal) {
animal.bathe()//开洗
}
}
fun main() {
val cat1 = Cat()
val cat2 = Cat()
val animals = listOf<Cat>(cat1, cat2) //将定义的两只猫装入一个集合
val petShop = PetShop<Cat>(animals) //将宠物送到宠物店
batheAll(petShop) //不再报错
//运行结果:bathing
//运行结果:bathing
}
逆变(in)
与协变的out关键字对应的是in关键字,in关键字与out关键字有相反的功能,可以使得Xxx<B>不是Xxx<A>的子类型。
open class Animal
class Cat : Animal()
class Dog : Animal()
class PetShop<in T> {
fun feed(animal: T) {
if (animal is Cat) {
println("Feed the cat")
} else if (animal is Dog) {
println("Feed the dog")
}
}
}
fun feedCat(petShop: PetShop<Cat>): Unit {
petShop.feed(Cat())
}
fun main() {
feedCat(PetShop<Animal>())//运行结果:Feed the cat
}
示例中,feedCat方法需要传递的参数类型是PetShop<Cat>,但main方法中调用这个方法时,传入的是PetShop<Animal>,未出现报错且运行成功,可知PetShop<Animal>是PetShop<Cat>的子类型,但在定义Cat类时,Cat是Animal的子类型。这是由于PetShop类在定义时,使用了in关键字,是的泛型参数产生了逆变。
对于协变和逆变,需要注意以下几点:
- out、in关键字可以出现在泛型类型或泛型接口的泛型参数声明上,不能出现在泛型方法的泛型参数声明上;
- 泛型参数T在使用了out、in关键字之后,不能声明成val或者var类型的变量。
点变型
除了在类或接口中定义泛型参数时使用out、in关键字,还可以在泛型参数出现的具体位置使用这两种关键字。
open class Fruit(val name: String)
open class Mammal(val name: String)
class Banana : Fruit("Banana")
class Pear : Fruit("Pear")
class Lion : Mammal("Lion")
class Tiger : Mammal("Tiger")
class Forest<T>(var content: T)//在此处使用了var,无法使用out、in关键字
fun printFruit(forest: Forest<out Fruit>) {//泛型参数出现的位置使用了out关键字
println(forest.content.name)
}
fun printMammal(forest: Forest<out Mammal>) {//泛型参数出现的位置使用了out关键字
println(forest.content.name)
}
fun main() {
val bananaForest = Forest<Banana>(Banana())
val pearForest = Forest<Pear>(Pear())
val lionForest = Forest<Lion>(Lion())
val tigerForest = Forest<Tiger>(Tiger())
printFruit(bananaForest) //运行结果:Banana
printFruit(pearForest) //运行结果:Pear
printMammal(lionForest) //运行结果:Lion
printMammal(tigerForest) //运行结果:Tiger
}
在main方法中定义变量时,传入Forest的参数类型Banana和Pear、Lion和Tiger,分别是Fruit和Mammal的子类型。而在定义输出方法(printFruit、printMammal)的传入的泛型参数时,使用了out关键字,这使得这两个泛型参数发生了协变。由于子类型的关系,此时Forest<Banana>和Forest<Lion>变成了Forest<Fruit>的子类型,Forest<Lion>和Forest<Tiger>变成了Forest<Mammal>的子类型。因此在main方法使用两个输出方法时,不会出现错误。
泛型擦除与实化类型
泛型擦除
在JVM虚拟机中,没有泛型,泛型类的种类在编译时都会被擦除。
所谓的擦除指的是在定义一个泛型时,例如List<String>类型,在运行时只是一个List,并不体现String类型。
fun main() {
val list1 = listOf("a", "b", "c")
val list2 = listOf(1, 2, 3)
println(list1.javaClass)//运行结果:class java.util.Arrays$ArrayList
println(list2.javaClass)//运行结果:class java.util.Arrays$ArrayList
println(list1.javaClass == list2.javaClass)//运行结果:true
}
示例中,获取了list1和list2的数据类型,两者相同,说明List<String>和List<String>在程序运行期间都是相同的类型。
泛型通配符(*)
在Java中,若不知道泛型的具体类型时,使用“?”通配符来代替具体的类型。
在Kotlin中,使用“*”,这个符号被称为泛型通配符,他只能在“<>”中使用。
open class Food(val name: String)
open class Flower(val name: String)
class Rice : Food("Rice")
class Rose : Flower("Rose")
class Container<T>(var content: T)
fun printInfo(container: Container<*>) {
val content = container.content
if (content is Food) {
println(content.name + " is Food")
} else if (content is Flower) {
println(content.name + " is Flower")
}
}
fun main() {
val riceContainer = Container<Rice>(Rice())
val roseContainer = Container<Rose>(Rose())
printInfo(riceContainer)//运行结果:Rice
printInfo(roseContainer)//运行结果:Rose
}
示例中,定义了一个printInfo方法来输出Contain中包含的信息,可以传入Container<out Rice>或是Container<out Rose>,由于不能明确需要传入的参数类型,因此使用泛型通配符代替。
星投影
当对泛型的实参一无所知,但仍希望用安全的方法使用它时,此时可以使用星投影。
星投影是将泛型中的“*”,等价于泛型中,使用了out和in关键字的协变类型参数和逆变类型参数。
Kotlin提供了星投影语法,以自定义的泛型类A<T>为例来演示:
- A<out T>,T是一个具有上界TUpper的协变类型参数,A<*>等价于A<out TUpper>,意味着当T未知时,可以安全从A<*>获取TUpper的值
- A<in T>,T是一个逆变类型参数,A<*>等价于A<in Noting>,其中Noting类型表示没有任何值,意味着当T未知时,没有安全方式写入A<*>
- A<T>,T是一个具有上界TUpper的不型变类型参数,读取时等价于A<out TUpper>,写入时等价于A<in Noting>
若泛型类型具有多个类型参数,则每个参数类型都可以进行单独的星投影。以泛型类B<in T, out U>为例:
- 泛型类为B<*, String>,该泛型类等价于B<in Noting, String>
- 泛型类为B<Int, *>,该泛型类等价于B<Int, out Any?>
- 泛型类为B<*, *>,该泛型类等价于B<in Noting, out Any?>
实化类型(reified)
泛型在运行时会被擦除,这样就无法得知某一个反省形参在使用时具体是什么类型的泛型实参。
在Java中年,可以通过反射获取泛型的真实类型。在Kotlin中需要在内联函数(inline关键字定义的函数,Lambda编程内容)中使用reified关键字修饰泛型参数,这样的参数被称为实化类型。
reified关键字必须和内联的泛型函数同时使用,才可以获取泛型实参的类型。
inline fun <reified T> Any.isType(): Boolean {
if (this is T) {
return true
}
return false
}
fun main() {
println("abc".isType<String>()) //运行结果:true
println(123.isType<String>()) //运行结果:false
}