博客主页

1. 泛型类型参数

1.1 泛型函数和属性

在使用集合的库函数都是泛型的。我们来看下slice函数的定义:

//public fun 类型形参声明 List<接收者类型形参>.slice(indices: Iterable<Int>): List<返回类型的类型形参> 
public fun <T> List<T>.slice(indices: Iterable<Int>): List<T> 

在一个具体的列表上调用这个函数时,可以显式地指定类型实参,但大部分情况下不必这样做,因为编译器会推导出类型。

val letters = ('a'..'z').toList()

//显式地指定类型实参
println(letters.slice<Char>(0..2))
// [a, b, c]

// 编译器推导出这里的T是Char
println(letters.slice(10..13))
// [k, l, m, n]

先来看下filter函数的声明,它接收一个函数类型:(T) -> Boolean的参数

val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>("Bob", "Svetlana")

// filter函数的声明
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> 

readers.filter { it !in authors }

编译器推断T就是String,因为它知道函数应该在List<T> 上调用,而它的接收者readers的真实类型是List<String>

可以给类或者接口的方法、顶层函数,以及扩展函数声明类型参数。还可以声明泛型的扩展属性:

// 这个泛型扩展属性能在任何类型的元素列表上调用
val <T> List<T>.penultimate: T
    get() = this[size - 2]

// 类型T会被推导为Int
println(listOf(1, 2, 3, 4).penultimate)
// 3

1.2 声明泛型类

与java一样,在kotlin中也是通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口,这样就可以在类的主体内像其它类型一样使用泛型参数。

// List接口定义了类型参数 E
public interface List<out E> {
   // 在类或者接口内部,E 可以当作普通的类型使用
  public operator fun get(index: Int): E
  // ...
}

如果一个类继承了泛型类(或者实现了泛型接口),就等为基础类型的泛型形参提供一个类型实参,它可以是具体的类型或者一个类型形参:

// 这个类实现了List,并提供了具体类型实参 String
class StringList : List<String> {
    override fun get(index: Int): String  = ...
}

// ArrayList的泛型类型形参 T 就是List的类型实参
class ArrayList<T> : List<T> {
    override fun get(index: Int): T  = ...
}

StringList类被声明只能包含String元素,而类ArrayList指定了自己的类型参数T并指定为父类的类型实参。

一个类还可以把它自己作为类型实参引用。

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

public class String : Comparable<String> {
    public override fun compareTo(other: String): Int
}

String类实现了Comparable泛型接口,提供类型String给类型实参T。

1.3 类型参数约束

类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。

如果把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。
在java中,使用extends关键字:

<T extends Number> T sum(List<T> list)

在kotlin中,使用冒号(:)

//  通过在类型参数后指定上界来定义约束

// <类型参数 : 上界>
fun <T : Number> List<T>.sum(): T

如果指定了类型形参T的上界,就可以把类型T的值当作它的上界(类型)的值使用:

// 指定Number为类型形参的上界
fun <T : Number> oneHalf(value: T): Double {
    // 调用Number类中的方法
    return value.toDouble() / 2.0f
}

println(oneHalf(3))
// 1.5

再来看一个例子:T的上界是泛型类型Comparable<T>,String类继承了Comparable<String>,String就可以作为max函数的有效类型实参。

// 声明带类型参数约束的函数
fun <T : Comparable<T>> max(first: T, second: T): T {
    // 根据kotlin运算符约定会被编译成first.compareTo(second) > 0
    return if (first > second) first else second
}

println(max("kotlin", "java"))
// kotlin

1.4 让类型形参非空

如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。
没有指定上界的类型形参将会使用Any?这个默认的上界。

class Processor<T> {
    fun process(value: T) {
        // "value"是可空的,所以要用安全调用
        value?.hashCode()
    }
}

// 可空类型String?被用来替换T
val nullableStringProcessor = Processor<String?>()
// 使用null作为value实参的代码可以编译
nullableStringProcessor.process(null)

如果你想保证替换类型形参始终是非空类型,可以通过指定一个约束来实现。如果除了可空性之外没有任何限制,可以使用Any代替默认的Any?作为上界:

// 指定非空上界
class Processor<T : Any> {
    fun process(value: T) {
        // 类型T的值现在是非空的
        value.hashCode()
    }
}

约束<T : Any>确保了类型T永远都是非空类型。

2. 运行时的泛型:擦除和实化类型参数

JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。

2.1 运行时的泛型:类型检查和转换

与java一样,kotlin的泛型在运行时也被擦除了。例如:创建一个List<String>,在运行时只能看到它是一个List。

注意一点:擦除泛型类型信息是有好处的,应用程序使用的内存总量较小,因为要保存在内存中的类信息更少。

可以使用特殊的星号投影语法来检查,一个值是否是列表,而不是set或者其他对象。

if (value is List<*>) { ... }

可以认为它就是拥有未知类型实参的泛型类型(或者类比于java的List<?>)。

as和as?转换中可以使用一般的泛型类型。如果该类有正确的基础类型但类型实参是错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的,这样写,编译器会发出Unchecked cast(未受检转换)的警告

// 对泛型类型做类型转换
fun printSum(c: Collection<*>) {
    // 这里会有警告 Unchecked cast:List<*> to List<Int>
    val intList = c as? List<Int>
        ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

printSum(listOf(1, 2, 3))
// 6

printSum(listOf("a", "b", "c"))
// java.lang.ClassCastException: 
// java.lang.String cannot be cast to java.lang.Number

如果传一个错误类型的值,运行时就会抛出ClassCastException异常。

// 对已知类型实参做类型转换
fun printSum(c: Collection<Int>) {
   if (c is List<Int>) {
       println(c.sum())
   }
}

2.2 声明带实化类型参数的函数

kotlin泛型在运行时会被擦除。在调用泛型函数的时候,在函数体中不能决定调用它用的类型实参:

// Error: Cannot check for instance of erased type: T
fun <T> isA(value: Any) = value is T

只有一种例外可以避免这种限制:内联函数。内联函数的类型形参能够被实化,意味着可以在运行时引用实际的类型实参。

如果把isA函数声明成inline并用reified标记类型参数,就可以用该函数检查value是不是T实例:

// 声明带实化类型参数的函数

// 现在代码可以编译了
inline fun <reified T> isA(value: Any) = value is T

一个实化类型参数能发挥作用的最简单的例子就是标准库函数filterIsInstance。这个函数接收一个集合,选择其中那些指定类的实例,然后返回这些选中的实例。

// 使用标准库函数filterIsInstance

val list = listOf("one", 2, "three")
println(list.filterIsInstance<String>())
// [one, three]

通过指定<String>作为函数的类型实参,表明只关心字符串,所以函数的返回类型是List<String>
这种情况下,类型实参在运行时是已知的,函数filterIsInstance使用它来检查列表中的值是不是指定为该类型实参的类的实例。

// filterIsInstance函数的定义

// “reified”声明了类型参数不会在运行时被擦除
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
    return filterIsInstanceTo(ArrayList<R>())
}

public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
    // 可以检查元素是不是指定为类型实参的类的实例
    for (element in this) if (element is R) destination.add(element)
    return destination
}

2.3 使用实化类型参数代替类引用

另一种实化类型参数的常见场景是为接收java.lang.Class类型参数的API构建适配器。

如:jdk中的ServiceLoader,它接收一个代表接口或者抽象类的java.lang.Class,并返回实现了该接口(或者继承了该抽象类)的类的实例。

// 使用标准的ServiceLoader java api加载一个服务

val serviceImpl = ServiceLoader.load(Service::class.java)

::class.java获取java.lang.Class对应的kotlin类,这和java中的Service.class是完全等同的。

接下来用带实化类型参数的函数重写这个例子:

// loadService函数定义
// 类型参数标记成了reified
inline fun <reified T> loadService(): ServiceLoader<T> {
    // 把T::class当成类型形参的类访问
    return ServiceLoader.load(T::class.java)
}

val serviceImpl = loadService<Service>()

可以简化Android上的startActivity函数:

// 可以使用实化类型参数来代替传递作为java.lang.Class的activity类
inline fun <reified T : Activity> Context.startActivity() {
   // 把T:class当成类型参数的类访问
   val intent = Intent(this, T:class.java)
   startActivity(intent)
}

// 调用方法
startActivity<DetailActivity>()

2.4 实化类型参数的限制

可以按照下面的方式使用实化类型参数:

  • 用在类型检查和类型转换中(is、!is、as、as?)
  • 使用kotlin反射API
  • 获取相应的java.lang.Class(::class.java)
  • 作为调用其他函数的类型实参

不能做下面这些事情:

  • 创建指定为类型参数的类的实例
  • 调用类型参数类的伴生对象的方法
  • 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
  • 把类、属性或者非内联函数的类型参数标记成reified

3. 变型:泛型和子类型化

变型:描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的,例如:List<String>List<Any>之间如何关联。

3.1. 为什么存在变型:给函数传递实参

假设有一个接收List<Any>作为实参的函数,把List<String>类型的变量传给这个函数是否安全?
如果函数添加或者替换了列表中的元素是不安全的,因为这样会产生类型不一致的可能性,否则它就是安全的。

下面这段代码是安全的,因为String类继承了Any,所以是安全的。

fun printContents(list: List<Any>) {
    println(list.joinToString())
}

printContents(listOf("abc", "cbd"))
// abc, cbd

看另一个函数,它修改列表(接收一个MutableList作为参数):

fun addAnswer(list: MutableList<Any>) {
    list.add(23)
}

然后把一个字符串列表传给这个函数,将发生什么呢?

val list = mutableListOf("abc", "cbd")
// type mismatch,编译通不过
addAnswer(list)

3.3 协变:保留子类型化关系

如果A是B的子类型,那么Producer<A>就是Producer<B>的子类型。我们说子类型化被保留了

在kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上 out 关键字即可:

// 类被声明成在T上协变
interface Producer<out T> {
    fun produce(): T
}

类型参数 T 上的关键宇 out 有两层含义:

  • 子类型化会被保留

Producer<Cat>Producer<Animal> 的子类型

  • T只能用在 out 位置

现在我们看看 List<Interface>接口。kotlin中的List是只读的,所以它只有一个返回类型为 T 的元素的方法 get ,而没有定义任何把类型为 T 的元素存储到列表中的方法。因此,它也是协变的。

// kotlin标准库中List的定义
public interface List<out E> : Collection<E> {
    // 只读接口只定义了返回 E 的方法。所以E在 ”out“ 位置
    public operator fun get(index: Int): E
    // .... 
}

类型形参不光可以直接当作参数类型或者返回类型使用,还可以当作另一个类型的类型实参。例如, List接口就包含了一个返回 List<T>的 subList方法。

public interface List<out E> : Collection<E> {
     // 这里的 E 也在 ”out“位置
     public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

不能把MutableList<E>在它的类型参数上声明成协变的,因为它既含有接收类型为E的值作为参数的方法,也含有返回这种值的方法(因此,E 出现 in 和 out 两种位置上)。

// MutableList不能在E上声明成协变的
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // 因为E用在了 “in” 位置
    override fun add(element: E): Boolean
}

其中构造方法的参数既不在 in 位置,也不在 out 位置。

如果你在构造方法的参数上使用了关键字 val 和 var,同时就会声明一个getter 和setter(如果属性是可变的)。因此,对只读属性来说,类型参数用在了 out 位置,而可变属性在 out 位置 in 位置都使用了它。

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { .. . }

上面这个例子中, T 不能用 out 标记,因为类包含属性 leadAnimal 的setter ,它在 in 位置用到了 T。注意:私有方法的参数既不在 in 位置也不在 out 位置。如果leadAnimal是私有的,可以使用协变。

3.4 逆变:反转子类型关系

看下Comparator接口:

interface Comparator<in T> { 
   // 在 “in” 位置使用了 T
   fun compare(el: T, e2: T): Int { ... }
}

在类型参数T上的in关键字意味着子类型化被反转了,而且 T 只能用在 in 位置。

一个类可以在一个类型参数上协变,同时在另外一个类型参数上逆变。

interface Functionl<in P, out R> { 
    operator fun invoke (p: P) : R
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。