上一篇,我们学习了Kotlin中的数据类和密封类,今天继续来学习Kotlin中的泛型。
泛型,即 “参数化类型”,将类型参数化,可以用在类,接口,方法上。
与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼。
class Box<T>(t: T) {
var value = t
}
interface IAnimal<T> {}
fun <T> initAnimal(param: T) {}
这里以泛型类为示例:
class Box<T>(t : T) {
var value = t
}
fun main(args: Array<String>) {
var boxInt = Box<Int>(10)
var boxString = Box<String>("Runoob")
println(boxInt.value)
println(boxString.value)
}
输出结果为:
10
Runoob
定义泛型类型变量,可以完整地写明类型参数,如果编译器可以自动推定类型参数,也可以省略类型参数。Kotlin 泛型函数的声明与 Java 相同,类型参数要放在函数名的前面:
fun <T> boxIn(value: T) = Box(value)
// 以下都是合法语句
val box4 = boxIn<Int>(1)
val box5 = boxIn(1) // 编译器会进行类型推断
在调用泛型函数时,如果可以推断出类型参数,可以省略泛型参数。
以下实例创建了泛型函数 doPrintln,函数根据传入的不同类型做相应处理:
fun main(args: Array<String>) {
val age = 23
val name = "runoob"
val bool = true
doPrintln(age) // 整型
doPrintln(name) // 字符串
doPrintln(bool) // 布尔型
}
fun <T> doPrintln(content: T) {
when (content) {
is Int -> println("整型数字为 $content")
is String -> println("字符串转换为大写:${content.toUpperCase()}")
else -> println("T 不是整型,也不是字符串")
}
}
输出结果为:
整型数字为 23
字符串转换为大写:RUNOOB
T 不是整型,也不是字符串
泛型约束表示我们可以指定泛型类型(T)的上界,即父类型,默认的上界为Any?,如果只有一个上界可以这样指定:
fun <T : Animal<T>> initAnimal(param: T) {}
即Animal就是上界类型,这里使用了:,在 Java 中对应extends关键字,如果需要指定多个上界类型,就需要使用where语句:
fun <T> initAnimal(param: T) where T : Animal<T>, T : IAnimal<T> {}
Kotlin 为泛型声明执行的类型安全检测仅在编译期进行, 运行时实例不保留关于泛型类型的任何信息。这一点在 Java 中也是类似的。例如,Array、Array的实例都会被擦除为Array<*>,这样带来的好处是保存在内存中的类型信息也就减少了。
由于运行时泛型信息被擦除,所以在运行时无法检测一个实例是否是带有某个类型参数的泛型类型,所以下面的代码是无法通过编译的(Cannot check for instance of erased type: Array):
fun isArray(a: Any) {
if (a is Array<Int>) {
println("is array")
}
}
但我们可以检测一个实例是否是数组,虽然 Kotlin 不允许使用没有指定类型参数的泛型类型,但可以使用星投影*(这个后边会说到):
fun isArray(a: Any) {
if (a is Array<*>) {
println("is array")
}
}
型变是泛型中比较重要的概念,首先我们要知道 Kotlin 中的泛型是不型变的,这点和 Java 类似。那什么是型变呢,看个例子:
open class Animal
class Dog : Animal()
val array1: Array<Dog > = arrayOf(Dog (), Dog (), Dog ())
val array2: Array<Animal> = array1
你会发现第二个赋值语句会有错误提示,Type mismatch. Required:Array
为什么Array无法正常的赋值,而List、Set、Map可以呢?如下代码,编译器不会有错误提示的:
val list1: List<Dog> = listOf(Dog(), Dog(), Dog())
val list2: List<Animal> = list1
我们可以对比一下Array和List在源码中的定义:
public class Array<T> {}
public interface List<out E> : Collection<E> {}
可以看到List的泛型类型使用了out修饰符,这就是关键所在了。这就是 Kotlin 中的声明处型变,用来向编译器解释这种情况。
关于out修饰符我们可这样理解,当类、接口的泛型类型参数被声明为out时,则该类型参数是协变的,泛型类型的子类型是被保留的,它只能出现在函数的输出位置,只能作为返回类型,即生产者。带来的好处是,A是B的父类,那么List
使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型:
// 定义一个支持型变的类
class Runoob<out A>(val a: A) {
fun foo(): A {
return a
}
}
fun main(args: Array<String>) {
var strCo: Runoob<String> = Runoob("a")
var anyCo: Runoob<Any> = Runoob<Any>("b")
//由于Runoob中的泛型A使用了out,泛型类型的子类型是被保留的,这里可以赋值
anyCo = strCo
println(anyCo.foo()) // 输出 a
}
我们修改下上边List赋值的代码:
val list1: List<Animal> = listOf(Animal(), Animal(), Animal())
val list2: List<Dog> = list1
即反过来赋值,由于B并不是A的父类,会有Type mismatch. Required:List
关于in修饰符我们可这样理解,当类、接口的泛型类型参数被声明为in时,则该类型参数是逆变的,泛型类型的父类型是被保留的,它只能出现在函数的输入位置,作为参数,只能作为消费类型,即消费者。
其实 Kotlin 中的Comparable接口使用了in修饰符:
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
fun test(a: Comparable<A>) {
//可以赋值不会报错
val b: Comparable<B> = a
}
所以in修饰符和out修饰符的作用看起来的相对的,A是B的父类,那么Comparable可以是Comparable的父类,体会下区别。
为了能将Array
val array1: Array<Dog> = arrayOf(Dog(), Dog(), Dog())
val array2: Array<out Animal> = array1
这就是使用处型变,相比声明处型变,使用处型变就要复杂些,为了完成对应的需求,需要每次使用对应类时都添加型变修饰符。而声明处型变在类、接口声明时就做好了这些工作,因而代码会更加简洁。
再看一个数组拷贝的函数:
fun copy(from: Array<Animal>, to: Array<Animal>) {
for (i in from.indices) {
to[i] = from[i]
}
}
我们试着执行如下的拷贝操作:
val array1: Array<Dog> = arrayOf(Dog(), Dog(), Dog())
val array2: Array<Animal> = arrayOf(Animal(), Animal(), Animal())
copy(array1, array2)
同样的问题,由于泛型默认不型变的原因,copy(array1, array2)并不能正常工作。
回想一下,在 Java 中类似的问题可以使用通配符类型参数解决这个问题:
public void copy(ArrayList<? extends A> from, ArrayList<? super A> to) {}
那么在 Kotlin 中我们自然想到的是型变修饰符了:
修改上边的 copy函数:
fun copy(from: Array<out Animal>, to: Array<Animal>) {
for (i in from.indices) {
to[i] = from[i]
}
}
这样copy函数就能正常的工作了。使用处型变其实也是一种类型投影,from、to此时都是一个类型受限的投影数组,它们只能返回、接收指定类型的数据。
稍微修改下Dog和Animal类:
open class A {
open fun println(){
println("A")
}
}
class B : A() {
override fun println() {
println("B")
}
}
val array1: Array<B> = arrayOf(B(), B(), B())
val array2: Array<A> = arrayOf(A(), A(), A())
copy(array1, array2)
array2.forEach { it.println() }
打印结果:
B
B
B
可以看到原来array2中的对象已经成功被替换。
有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓"安全地使用"是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型。
对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection):
如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface Function
注意: 星号投射与 Java 的原生类型(raw type)非常类似, 但可以安全使用
关于星号投射,其实就是*代指了所有类型,相当于Any?
class A<T>(val t: T, val t2 : T, val t3 : T)
class Apple(var name : String)
fun main(args: Array<String>) {
//使用类
val a1: A<*> = A(12, "String", Apple("苹果"))
val a2: A<Any?> = A(12, "String", Apple("苹果")) //和a1是一样的
val apple = a1.t3 //参数类型为Any
println(apple)
val apple2 = apple as Apple //强转成Apple类
println(apple2.name)
//使用数组
val l:ArrayList<*> = arrayListOf("String",1,1.2f,Apple("苹果"))
for (item in l){
println(item)
}
}
输出结果:
com.example.kotlindemo.Apple@6e2c634b
苹果
String
1
1.2
com.example.kotlindemo.Apple@7c3df479
今天的学习笔记就先到这里了,下一篇我们将继续学习Kotlin中的object关键字。
老规矩,喜欢我的文章,欢迎素质三连:点赞,评论,关注,谢谢大家!