与Java泛型相同,Kotlin同样提供了泛型支持。对于简单的泛型类、泛型函数的定义,Kotlin 与 Java 的差别不大 。Kotlin 泛型的特色功能是型变支持, Kotlin 提供了声明处型变和使用处型变两种支持 ,而Java 只支持使用处型变。
泛型入门
定义泛型接口、类
可以为任何类、接口增加泛型声明。下面自定义一个 Apple 类,这个Apple类就可以包含一个泛型声明。
//定义 Apple 类时使用了泛型声明
open class Apple<T> {
//使用泛型 T 定义属性
open var info: T?
constructor() {
info = null
}
//下面方法中使用泛型 T 来定义构造器
constructor(info: T) {
this.info = info
}
}
fun main(args: Array<String>) {
//由于传给泛型 T 的是 String ,所以构造器的 参数只能是 String
var a1 :Apple<String> = Apple("苹果")
println(a1.info)
//由于传给泛型 T 的是 Int ,所以构造器的 参数只能是 Int
var a2:Apple<Int> = Apple(10)
println(a2.info)
//由于构造器的参数是 Double,因此系统可推断出泛型形参为 Double 类型
var a3 = Apple(3.5)
println(a3.info)
}
从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或者从该父类派生子类。需要指出的是,当使用这些接口、父类时不能再包含泛型形参。例如,下面代码就是错误的(Apple类必须有 open修饰才能派生子类)。
//定义类 A 继承 Apple 类, Apple 类不能还使用泛型形参
class A: Apple<T>()
在定义类、接口、方法时可以声明泛型形参,在使用类、接口、 方法时应该为泛型形参传入实际的类型。
如果想从 Apple类派生一个子类,则可以改为如下代码 :
class A: Apple<String> ()
与Java不同的是,Kotlin要求始终为泛型参数明确地指定类型,而不管是通过显式指定,还是让系统进行推断。例如,如下代码是错误的。
//系统无法推断出 T 是何种类型,因此编译报错
var a4 = Apple ()
//使用 Apple 类时,没有为泛型 T 传入实际的类型参数,编译报错
public class A extends Apple
如果从 Apple<String>类派生子类,则在 Apple 类中所有使用泛型 T 的地方都将被替换成 String类型,即它的子类将会继承 String类型的info属性。如果子类需要重写父类的属性或方法,就必须注意这一点。
class A1: Apple<String>() {
override var info: String? =null
get() ="子类"+ super.info
}
型变
Java 的泛型是不支持型变的,Java采用通配符来解决这个问题;而 Kotlin 则采用安全的型变代替了 Java 的通配符。
泛型型变的需要
首先回顾一下 Java 泛型的特征 : Java 的泛型是不支持型变的。通俗地说,List<String>并不是 List<Object>的子类,因此 List<String>不能直接赋值给 List<Object>。
但 Java取消型变之后,程序变得非常麻烦。比如 Java的 Collection中有一个 addAll()方法, 该方法负责将另一个集合中的所有元素添加到本集合内。假如 Java将该方法定义为如下形式:
interface Collection<E> {
void addAll(Collection<E> items);
}
那么如下代码也是不能运行的。
Set<Number> numSet = new HashSet<> () ;
Set<Integer> intSet =new HashSet<>();
numSet.addAll(intSet);
为了处理型变的需要,Java采用通配符方式进行处理,它将 addAll()方法定义为如下形式:
addAll(Collection<? extends E> c)
此时 addAll()方法的参数类型是指定上限的类型,其本质就是为了支持型变,因此上面代码可以正常运行。
泛型存在如下规律:
- 通配符上限(泛型协变)意味着从中取出( out)对象是安全的,但传入对象( in)则不可靠。
- 通配符下限(泛型逆变)意味着向其中传入(in)对象是安全的,但取出对象(out)则不可靠。
Kotlin利用上面两个规律,抛弃了泛型通配符语法,而是利用 in、 out来让泛型支持型变。
声明处型变
Kotlin 处理泛型型变的规则很简单:
- 如果泛型只需要出现在方法的返回值声明中(不出现在形参声明中),那么该方法就只是取出泛型对象,因此该方法就支持泛型协变(相当于通配符上限):如果一个类的所有方法都支持泛型协变,那么该类的泛型参数可使用 out修饰。
- 如果泛型只需要出现在方法的形参声明中(不出现在返回值声明中),那么该方法就只是传入泛型对象,因此该方法就支持泛型逆变(相当于通配符下限):如果一个类的所有方法都支持泛型逆变,那么该类的泛型参数可使用 in修饰。
下面程序先定义一个支持泛型协变的类。
class User<out T> {
//此处不能用 var,否则就有 setter 方法
// setter 方法会导致 T 出现在方法形参中
val info: T
constructor(info: T) {
this.info = info
}
fun test(): T {
println("执行 test方法")
return info
}
}
fun main(args: Array<String>) {
//此时 T 的类型是 String
var user = User("kotlin")
println(user.info)
//对于 u2 而言,它的类型是 User<Any>,此时T的类型是Any
//由于程序声明了T支持协变,因此User<String>可当成 User<Any>使用
var u2 =user
}
上面程序中代码声明了一个泛型类,且使用了out 修饰泛型形参,因此在该User类的内部,T只能出现在方法的返回值声明中,不能出现在方法的形参声明中。所以如果用T为User类声明属性,则只能声明为只读属性,否则setter方法 的形参类型是T,这就不符合要求了。
一 旦声明了泛型类支持协变,程序即可安全地将 User<String>、 User<Int>赋值给 User<Any>,只要尖括号中的类型是Any 的子类即可。
下面程序再定义 一个支持泛型逆变的类。
class Item<in T> {
fun foo(t: T) {
println(t)
}
}
fun main(args: Array<String>) {
//此时 T 的类型是 Any
var item = Item<Any>()
item.foo(200)
var im2 : Item<String> = item
// im2 的实际类型是 Item<Any>,因此它的foo参数只要是Any即可
// 但我们声明了 im2 的类型为 Item<String>
// 因此传入的参数只可能是 String,所以程序肯定是安全的
im2.foo("Kotlin")
}
上面代码声明了一个泛型类, 且使用了in修饰泛型形参,因此在该Item类的内部,T只能出现在方法的形参声明中,不能出现在方法的返回值声明中 。
一旦声明了泛型类支持逆变 , 程序即可安全地将 ltem<Any>、 ltem<CharSequence>赋值给 User<Sting>, 只要尖括号中的类型是String 的父类即可。
通过上面介绍不难发现 , Kotlin的处理规则很简单:
- 如果泛型 T(或其他字母)只出现在该类的方法的返回值声明中 (T代表的是传出值),那么该泛型形参即可使用 out修饰 T。
- 如果泛型 T(或其他字母)只出现在该类的方法的形参声明中(T代表的是传入参数),那么该泛型形参即可使用 in修饰 T。
使用out修饰泛型的类支持协变,也就是可以将User<String>、User<Int>当成 User<Any>处理,只要尖括号中的类型是 Any 的子类即可; 使用 in修饰泛型的类支持逆变,也就是可以将Item<Any>、 ltem<CharSequence>当成 Item<String>处理,只要尖括号中的类型是String的父类就行。
上面定义的 User、 Item 类都是在声明时使用 out 或 in 指定泛型支持型变的,因此这种方式被称为“声明处型变”。
使用处型变:类型投影
声明时型变虽然方便,但它有一个限制 :要么该类的所有方法都只用泛型声明返回值类型 (此时可用out声明型变),要么所有方法都只用泛型声明形参类型(此时可用 in声明型变)。 如果一个类中有的方法使用泛型声明返回值类型,有的方法使用泛型声明形参类型,那么该类就不能使用声明处型变。典型的例子就是 Kotlin 的 Array 类 ,它无法使用声明处型变。因此该 Array 类包含如下两个方法:
class Array<T>(val size: Int) {
fun get(index: Int) : T {
///* ...... */
}
fun set(index: Int, value: T) {
///* ..... */
}
}
上面 Array 类的泛型参数 T 既要出现在 get()方法的返回值声明中,也要出现在 set()方法的形参声明中,因此该 Array类的泛型T既不能用out修饰,也不能用in修饰。
而 List 集合则不同,由于 List 集合是一个只读集合,程序只需要从 List 集合中取出元素(不能添加元素),因此 T 只会出现在 List集合方法的返回值声明中。所以List集合可定义为支持协变。 List 的源代码片段如下 :
public interface List<out E> : Collection<E> {
}
如果不能使用声明处型变,则还可使用 Kotlin 提供的“使用处型变”。所谓使用处型变,就是在使用泛型时对其使用out或in修饰。
由于 Array类本身不支持声明处型变,因此这里将会以 Array为例来讲解使用处型变。下面先看使用处协变(使用 out修饰)。
fun copy(from: Array<out Any>, to: Array<Any>) {
val length = if (from.size < to.size) from.size else to.size
for (i in 0 until length) {
to[i] = from[i]
}
}
fun main(args: Array<String>) {
var arr1 = arrayOf(1,5,7,0)
var arr2:Array<Any> = arrayOf(4,78,23,9,10)
copy(arr1,arr2)
println(arr2.contentToString())
}
留意上面程序中 copy()函数的 from参数的声明,该 from 参数的类型是 Array<out Any>, 这就是使用处协变。也就是说,程序传入该from 参数的可以是 Array<Int>、 Array<String>等各种类型,只要尖括号中的类型是 Any 的子类即可,因此程序中from参数的是 Array<Int>。
需要说明的是,如果将from参数声明为Array<out Any>类型,那么就意味着只能安全地从该from参数代表的数组中取元素,而不能将元素添加到from 数组中,道理很明显 : 我们无法预测实际传给from参数的是 Array<Int>还是 Array<String>。
下面我们以 Array 为例来讲解使用处逆变(使用in修饰)。
fun fill(dest:Array<in String>,value:String){
if(dest.size>0){
dest[0] = value
}
}
fun main(args: Array<String>) {
var arr:Array<CharSequence> = arrayOf("4","kotlin","java")
fill(arr,"xxx")
println(arr.contentToString())
var intArr:Array< Int> = arrayOf(1,3,5,7,9)
println(intArr.contentToString())
intArr.set(0,34)
var numArr:Array<Number> = arrayOf(3,4,5,1.4,2.8)
//Array 不支持声明处型变,编译错误
intArr =numArr
println(intArr.contentToString())
}
星号投影
星号投影是为了处理 Java 的原始类型,比如如下Java代码:
ArrayList list = new ArrayList() ;
虽然Java的List、 ArrayList 都有泛型声明,但程序并没有为它们传入类型参数,这在 Java 程序中是允许的。这种用法被称为“原始类型”。
但在 Kotlin 中要写成如下形式。
fun main(args: Array<String>) {
//〈*〉必不可少,相当于 Java 的原始类型
var list: ArrayList<*> = arrayListOf(1, "str")
println(list)
}
关于星号投影,下面给出一些示例说明。
- 假如定义了支持声明时型变的 Foo<out T>类,该泛型支持声明时协变,因此其中T是一个具有上限的协变类型参数, Foo<>等价于 Foo<outAny?>。这意味着当 T未知时,我们可以安全地从 Foo<>读取 Any?类型的值。
- 假如定义了支持声明时型变的 Foo<in T> 类,该泛型支持声明时逆变,因此其中T是一个逆变类型参数, Foo<>等价于 Foo<in Nothing>。这意味着当T未知时,我们不能以任何安全的方式向 Foo <>写入值。
- 假如定义了不支持声明时型变的 Foo<T>类,该泛型不支持型变。这意味着当T未知时, Foo<>在读取值时等价于 Foo<out Any?>,在写入值时等价于 Foo<in Nothing> (即不能以任何安全的方式向 Foo <>写入值)。
泛型函数
前面介绍了在定义类、接口时可以使用泛型形参,在该类、接口的方法定义和属性定义中, 这些泛型形参可被当成普通类型来用。在另外一些情况下,在定义类、接口时没有使用泛型形参,但在定义方法时想自己定义泛型形参,这也是可以的, Kotlin 提供了对泛型函数的支持。
泛型函数的使用
所谓泛型函数,就是在声明函数时允许定义一个或多个泛型形参,泛型形参要用尖括号括起来,整体放在 fun 与函数名之间。泛型函数的语法格式如下:
- fun <T,S>函数名(形参列表):返回值类型{
//函数体...
}
把上面泛型函数的语法格式和普通函数的语法格式进行对比,不难发现泛型函数的函数签名比普通函数的函数签名多了泛型声明,函数形参声明以尖括号括起来,多个函数形参之间以逗号(,)隔开,所有的函数形参声明都放在 fun关键字和函数名之间。
例如,如下程序示范了泛型函数的用法。
fun <T> copy(from:List<T>,to:MutableList<in T>){
for (ele in from){
to.add(ele)
}
}
fun main(args: Array<String>) {
var strList = listOf("ss","ddd")
var objList:MutableList<Any> = mutableListOf(1,2,"ss")
//指定泛型函数的 T为 String类型
copy<String>(strList , objList)
println(objList)
var intList = listOf (7, 13, 17, 19)
//不显式指定泛型函数的 T 的类型,系统推断出 T 为 Int 类型
copy (intList , objList)
println(objList)
}
上面代码在 fun 和 copy 函数名之间声明了泛型:<T>,这样即可在该函数的形参声明或返回值声明中使用 T 来代表类型 。
声明了泛型函数之后,调用泛型函数时可以在函数名后用尖括号传入实际的类型,如上面代码所示:也可以在调用泛型函数时不为泛型参数指定实际的类型,而是让系统自动推断出泛型参数的类型。
泛型函数也可用于扩展函数:
//为泛型形参 T 扩展方法
fun <T> T.toBookString(): String {
return "《${this.toString()}》"
}
fun main(args: Array<String>) {
val a = 2
//显式指定泛型函数的 T 为 Int 类型
println(a.toBookString())
//不显式指定泛型函数的 T 的类型,系统推断出 T 为 Double 类型
println(3.4.toBookString())
}
具体化类型参数
Kotiin允许在内联函数(使用 inline修饰的函数)中使用 reified修饰泛型形参,这样即可将该泛型形参变成一个具体化的类型参数。 一旦将泛型形参变成具体化的类型参数,接下来在该函数中就可以像使用普通类型一样使用该类型参数,包括使用 is、 as 等运算符。
例如,我们要从某个 List 集合中查找第一个指定类型的元素,由于程序需要根据指定类型来查找数据,所以最容易想到的做法是,定义一个类型来作为参数 。
val db = listOf("ss", "rrr", java.util.Date(), 1111)
fun <T> findData(clazz: Class<T>): T? {
for (ele in db) {
if (clazz.isInstance(ele)) {
@Suppress("UNCHECKED_CAST")
return ele as? T
}
}
return null
}
fun main(args: Array<String>) {
println(findData(Integer::class.java))
println(findData(java.lang.Double::class.java))
}
上面代码确实可以实现我们的需求,但是这种方式未免太不优雅了,因为我们知道泛型形参本身就是类型参数,当程序调用该函数时完全可通过泛型形参来传入类型参数,何必还要通过函数的参数来传入类型呢?
此时就可考虑使用 reified 修饰内联函数的泛型形参,这样就可直接在函数中使用该类型形参,从而避免用户通过函数的参数来传入类型。例如
//使用 reified 修饰泛型形参,使之成为具体化的类型参数
inline fun <reified T> findData(): T? {
for (ele in db) {
if (ele is T) {
return ele
}
}
return null
}
fun main(args: Array<String>) {
println (findData<Int> ())
println(findData<Double>())
}
设定类型形参的上限
Kotlin 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,
用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。下面程序示范了这种用法。
class Apple1<T : Number> {
var col: T
constructor(col: T) {
this.col = col
}
}
fun main(args: Array<String>) {
//显式指定泛型函数的 T 是Int 类型
var ai = Apple<Int>(2)
//显式指定泛型函数 的 T 是 Double 类型
var ad: Apple<Double> = Apple(3.3)
}
上面程序定义了一个 Apple 泛型类,该 Apple 类的类型形参的上限是 Number 类,这表明使用 Apple 类时为 T 形参传入的实际类型参数只能是 Number 或 Number 类的子类。
网友评论