Kotlin泛型,有你想了解的一切-灵析社区

德州安卓

前言

在Java中就有泛型的概念,但是一直没有做个统一的梳理,里面的很多知识点也补了解其细节,所以本章内容就准备好好梳理一下。

为了更好的理解,我直接准备从最简单的泛型使用到泛型擦除、Kotlin中的实化类型参数、协变、逆变、点变型等等都介绍一遍。

正文

话不多说,直接开整。

泛型基本概念

我们平时总是泛型,那泛型是什么呢?

泛型就是可以定义带类型参数的类型,当这种类型的实例被创建出来时,类型形参会被替换为类型实参的具体类型

所以这里重点是类型参数,关于参数我们很熟悉,比如方法的参数在定义方法时就是形参,调用时就是传递的实参给方法,那类型参数就是我们平时在类或者方法中经常看见的T,这个T就是类型形参

//这里E就是类型形参
public interface MutableList<E> : List<E>
//这里的String就类型实参
val list: MutableList<String> = mutableListOf()

这里就和我们平时调用方法一样需要传递参数,只不过这里传递的参数是类型而已。

Kotlin要求类型实参被显示说明

这里就会和Java不一样的地方了,既然我创建实例需要传参,按理说这个类型实参是必须要传递的,但是Java的泛型是1.5之后才有的,所以可以在创建泛型类实例时不进行传递实参

//这里Java代码中,tempList的类型就是List
List tempList = new ArrayList();

不过这种写法在Kotlin中是不允许的,因为Kotlin的创建之初就有泛型的概念,要不显示的指明泛型参数的类型要不通过自动推导

//已经指明类型参数就是String
val list: MutableList<String> = mutableListOf()
//能自动推导出类型参数是String
val list1 = mutableListOf("a","b")

但是下面代码是无法通过IDE编译:

//这里无法知道类型参数,会直接报错
val errorList = listOf()

对于Kotlin这样必须提供类型参数的实参,也是极大的能减少平时代码错误。

声明泛型函数

看一下如何声明泛型函数,在我们的集合类中,有很多泛型函数,我们来看一个。

//这里的 <T> 就是定义了一个类型形参 
//接收者和返回者都用了这个形参
public fun <T> List<T>.slice(indices: IntRange): List<T> {
    if (indices.isEmpty()) return listOf()
    return this.subList(indices.start, indices.endInclusive + 1).toList()
}

这里没啥说的,就是通过在fun关键字后和方法名前定义类型形参即可

那如何使用呢,把一个类型实参传递给这个函数,在之前的mutableList()函数其实我们就知道了,直接放在函数名后即可:

//类型自动推导,List的类型实参是String
val list1 = mutableListOf("a","b")
//显示的给slice函数传递一个类型实参
val newList = list1.slice<String>(0 .. 1)

所以泛型函数可以是在接口或者类的函数,顶层函数,扩展函数,没有什么限制

声明泛型属性

泛型属性就和泛型函数有点区别,它只能定义为扩展属性,不能定义为非扩展属性

//定义一个扩展属性last
val <T> List<T>.last : T
    get() = this.last()

比如上面代码给List定义了一个扩展属性last,但是你不能定义非扩展属性:

//类中定义这个 编译不过
val <E> e: E

上面代码肯定无法编译,因为不能在一个类的属性中存储多个不同类型的值

声明泛型类

声明泛型类也非常简单,和接口一样,把需要定义的类型参数通过<>放在类名后面即可,

//直接在类名后面加上<>定义类型形参
public interface List<out E> : Collection<E> {
     //方法内部可以使用这个形参
    public operator fun get(index: Int): E

这里没啥可说的。

类型参数约束

类型参数约束可以约束泛型类或者泛型方法的类型实参的类型

这里也非常容易理解,也就是约束这个类型参数的范围,在Java中使用extends来表达,在Kotlin中直接使用冒号<T:Number>就说明这个类型参数必须是Number或者Number的子类型

//定义类型参数约束条件,上界为Number
fun <T : Number> List<T>.sum(): Number{
    //...
}
//可以编译
val list0 = arrayListOf(1,2,3,4,5,6,7,8)
list0.sum()
//无法编译
val list1 = arrayListOf("a","b")
list1.sum()

这里也可以添加多个上界约束条件,比如同时定义类型T有2个上界A、B,那传入的类型实参必须是A、B的子类型。

让类型形参非空

前面说了可以添加类型参数的约束,如果不加约束,那类型T的默认上界是Any?,注意这里是可空的。

//这里没有约束,T的上界默认是Any?
class TestKT<T> {
    fun progress(t: T){
        //可以为空
        t?.hashCode()
    }
}
//所以调用的时候可以传入null
val stringNull = TestKT<String?>()
stringNull.progress(null)

由于Java是没有可空类型的概念,所以这里我想让类型非空,给指定一个上界 Any 即可。

//这里类型参数就不能传入可空类型实参
class TestKT<T : Any> {
    fun progress(t: T){
        t.hashCode()
    }
}

类型擦除

Java/Kotlin的泛型可以说是伪泛型,因为在运行时这个类型参数会被擦除,具体为什么要这么设计,很大原因是占用内存会更少。

这里就要明白基础类型的概念,比如 List< Int > 这种类型它的基础类型就是List,当代码运行时,只知道它是List,这里有个很经典的例子,我们来看一下:

//定义一个保存Int类型的List
val listInt = arrayListOf(1,2,3)
//定义一个保存String类型的List
val listString = arrayListOf("hello","java")
//在运行时,他们的class是相同的,都是List
if (listInt.javaClass == listString.javaClass){
    Logger.d("类型相同")
}
//通过反射可以往保存Int类型的List中添加String
listInt.javaClass.getMethod("add",Object::class.java).invoke(listInt,"aa")
//打印结果发现居然还加成功了,没有报错
Logger.d("listInt size = ${listInt.size}")

由上面这几行经典的代码我们能明白一个道理,就是泛型类实例在运行时是不会携带类型实参的,在运行时对于List< Int >和List< String >都是List,不知道它应该是保存何种类型的List。

类型检查

伴随了类型擦除这个特性,泛型类的类型就有了一些约束,比如类型检查

Kotlin中的类型检查是通过 is 函数,但是当类型参数被擦除时,你无法直接使用以下代码:

//定义一个ArrayList<String>
val listInt = arrayListOf(1,2,3)
//判断是否是ArrayList<String> 这代码无法编译
if (listInt is ArrayList<String>){
    
}

上面代码是无法编译的,会直接报错,因为在运行时根本携带不了类型实参,这里你只能判断它是不是List,如下代码:

val listInt = arrayListOf(1,2,3)
//可以判断这个变量是不是一个HashSet
if (listInt is HashSet<*>){

}

除了 is 函数受到限制外,还有就是as函数。

类型转换

同样伴随着类型擦除,类型转换 as 函数也受到限制,直接看个代码:

//接收Collection
fun printSum(c: Collection<*>){
    //这里当类型转换不成功时会主动抛出异常
    val intList = c as? List<Int> ?: throw IllegalArgumentException("期望是list")
    Logger.d("${intList.sum()}")
}

定义完函数,我们来进行调用:

val intSet = hashSetOf(1,2,3)
printSum(intSet)

这个代码会直接抛出异常,提示期望是list。

那我们就给他传递个list呢,但不是Int类型:

val stringList = arrayListOf("a","b")
printSum(stringList)

这行代码执行会抛出不一样的错误:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

其实想一下泛型的类型擦除就完全明白了,第一个调用肯定会失败,因为即使是擦除泛型,基类类型也不对。第二个调用代码能编译过,但是会有个警告:

因为编译器知道这个泛型参数会被擦除,这个操作是危险操作,所以后面还是会报出类型转换错误。

上面2个函数也是由编译器就直接在你编码阶段会提醒你的危险操作,防止出现由于泛型类型擦除导致的程序异常

Kotlin泛型

上面介绍了泛型的基本概念,下面来介绍一些关于Kotlin泛型的知识。

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

不同于Java,Kotlin可以实化类型参数,啥意思呢 也就是这个类型参数在运行时不会被擦除。

比如下面代码是编译不过的:

fun <T> isAny(value:Any): Boolean{
        //由于类型擦除,在执行时这个T的值是不携带的
    return value is T
}

但是Kotlin可以让上面的代码编译且执行成功,如下:

//函数声明为inline函数,且泛型参数使用reified来修饰
inline fun <reified T> isAny(value:Any): Boolean{
    //能执行成功
    return value is T
}

上面能保留类型实参的关键不仅仅是把类型实参使用reified修饰,也必须在内联函数中,因为编译器把实现内联函数的字节码插入到每一次调用发生的地方,所以每次调用带实化类型参数的函数时,编译器都知道调用中作为类型实参的确切类型,会直接把类型实参的值替换到字节码中,所以不会被擦除。

关于普通的内联函数,Java是可以调用的,但是无法进行内联。对于带reified类型参数的内联函数,Java是不能调用的,因为要实现实化功能,必须要内联,且做一些处理,Java编译器是无法内联的。

实化类型参数代替类引用

关于实化类型参数还有一个场景就是替代类引用,当一个函数参数接接收java.lang.Class类型时,可以用实化类型参数来优化,这个是啥意思呢 举个简单的例子,比如我们都是知道startActivity需要Intent,而Intent知道跳转类的class类型:

public Intent(Context packageContext, Class<?> cls) {
    mComponent = new ComponentName(packageContext, cls);
}

这里的Class就是类型的意思,既然我可以直接类型实化,那就可以改写一下:

//直接类型实化
inline fun <reified T: Activity> Context.StartActivity(){
    //类型实化,T就可以代表具体类型
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

调用也方便一点:

StartActivity<AboutUsActivity>()

实化类型参数的限制

既然Kotlin的实化类型参数很好用,但是它也有诸多限制,我们简单总结一下,可以做的操作:

    1. 用在类型检查和类型转换中 is和as
    2. 使用Kotlin反射API
    3. 获取相应的java.lang.Class
    4. 作为调用其他函数的类型实参 不可以的操作有:
    5. 不能创建指定为类型参数的类的实例
    6. 调用类型参数类的伴生对象的方法
    7. 在非内联函数中使用

变型

继续来聊泛型,这里引入了一个叫做变型的概念,什么是变型呢 这里要了解清楚,首先从字面意思来理解是变异/变化的泛型参数,用来描述拥有相同基础类型和不同类型实参的类型之间是如何关联的

这里比较绕,就比如List< T >的2个类型List< String >与List< Any >之间是如何关联的,这2个类型实参String是Any的子类型,那List< String >与List< Any >是什么关系呢,变型就是用来描述这个关系的。

为什么存在变型

既然知道了变型的概念,那这个在什么地方能用到这个变型呢 就是给参数传递实参的时候,可以直接看个例子:

//函数参数期望是List<Any>类型
fun printContent(list: List<Any>){
    Logger.d(list.joinToString())
}

定义完上面函数,我直接传入一个String类型集和会如何:

//这里能正常运行
val strings = arrayListOf("a","bc")
printContent(strings)

这里我们知道String是Any的子类型,所以这里期望是List< Any >,但是我传入的是List< String >没有问题也可以正常运行,那就说明这种关系一定是正常的吗 这可不一定,再看一个例子:

//传递进来一个可变list,然后加一个元素
fun addContent(list: MutableList<Any>){
    list.add(42)
}

同样定义完上面函数,把string类型集和传递:

会发现编译器直接报错,说期望的类型不一致,说明这时MutableList< String >和MutableList< Any >和上面的List< T >有很大区别,这就是为什么要讨论变型的原因。

其实上面2段代码我们都是把一个String类型实参的列表传递给期望是Any类型实参的列表,但是当函数中需要添加或者替换列表中的值就是不安全的,当仅仅是获取列表的值就是安全的,当然上面这个结论是我们很熟悉List和MutableList接口可以得出结论,所以我们要把这个问题推广到任何泛型类,而不仅仅是List,这就是为什么需要存在变型的原因。

类、类型、子类型

在继续说变型前,我们先理一下上面3个东西。

在我们平时使用中总是认为类就是类型,这当然是不对的。

对于非泛型类来说,比如 Int 是类,但是有2个类型,分别是 Int 和 Int? 类型,其中 Int? 类型不仅仅可以保存Int类的数据还可以保存null。

对于泛型类来说,一个类就对应着无数个类型了,比如List是类,它有List< Int >、List< String >等等无数个类型。

子类型定义非常重要,具体定义是:在任何时候如果需要类型A的值,都能够使用类型B的值来当做类型A的值,这样类型B就是类型A的子类型。

比如我定义一个Any 类型的变量a,这时用一个String 类型的值来当做a,这当然是可以的,这里就会有个问题,String是Any的子类,那类型也是这种关系吗

对于非泛型类来说,子类和子类型还真是差不多的东西,但是Kotlin有可空类型的存在,比如Int类型是Int?类型的子类型,但是它俩是一个类。对于泛型类,我们前面就举过例子当我需要List< Any >的时候我们可以传递List< String >给它,所以List< String >就是List< Any >的子类型,但是当期望是MutableList< Any >的时候却不能使用MutableList< String >类型值来给它,说明这俩不是子类型关系。

总结一下术语:对于List这种类来说,A是B的子类型,List< A >是List< B >的子类型,就说这个类是协变的;对于MutableList这种类来说,对于任意2种类型A和B,MutableList< A >即不是MutableList< B >的子类型也不是超类型,它就被称为在该类型参数上是不变型的

协变:保留子类型化关系

理解协变这种泛型参数的变型,必须要理解上面说的子类型化的概念,这样才好理解为什么放在in和out位置。

假如我有一个泛型类,就叫做List< T >,这时String类型是Any类型的子类型,那么List< String >就是List< Any >的子类型,这个类就是在T这个类型参数上是协变的,也就需要加个out来修饰T。

最常见的协变泛型类就是Kotlin中的List,代码如下:

public interface List<out E> : Collection<E> {
   
    override val size: Int
    override fun isEmpty(): Boolean
    }

不过这个例子大家可能都看烦了,其实List也是最好的一个协变例子,我们自己来写个类试试:

//定义动物类
open class Animal{
    fun feed(){
        Logger.d("zyh animal feed")
    }
}
//定义畜群,假设一个Herd就包含10只动物
class Herd<T : Animal>{
    //上界是Animal,所以底层实现也就是保存的是Animal类型
    val animal = Animal()

    fun getVa():T{
        return animal as T
    }
    //一个畜群有10个动物
    val size = 10
}
//给畜群投喂食物
fun feedAll(animals: Herd<Animal>){
    for (i in 0 until animals.size){
        //遍历取出Animal,然后投喂食物
        animals.getVa().feed()
    }
}
//定义猫,继承至动物
class Cat : Animal(){
    //需要清理
    fun clean(){
        Logger.d("zyh cat clean")
    }
}

上面函数看起来都没啥问题,我主要就是有个10只动物的畜群类,想给它们投喂食物,然后我现在想给猫群投喂食物:

//方法形参是猫群
fun feedCatHerd(cats: Herd<Cat>){
    for (i in 0 until cats.size){
        cats.getVa().clean()
    }
    //猫群属于畜群,所以我一次性投喂
    feedAll(cats)
}

这里看起来合情合理,不过代码却无法编译:

这里提示类型不匹配,所以这里就有问题了,我们再来梳理一下。

首先Herd是一个类,Cat类型是Animal类型的子类型,然后Herd泛型类变型是啥呢 就是Herd< Cat >和Animal< Animal >这2个类,我们也想Herd< Cat >是Herd< Animal >的子类型,这样我就可以处理上面的问题了,所以这种变型的关系就叫做协变,只需要在Herd的泛型参数前加个out,就说明该类在该参数是协变的。

//类型参数T 就是协变的
class Herd<out T : Animal>{

    val animal = Animal()

    fun getVa():T{
        return animal as T
    }

    val size = 10
}

in 位置 和 out 位置

上面我们熟悉了协变,就是给类型参数加上out关键字,那能不能把所有类型参数都定义为out呢 当然不可以,定义为out的类型参数在泛型类中只能被用在out位置,其实这里的out位置也就是返回值的位置,或者叫做生产者的位置,也就是生成类型为 T 的值

//比如这里的例子
interface Transformer<T>{
    //方法参数的位置就是 in位置
    fun transform(t: T): T
    //方法返回值的位置就是 out位置
}

所以在类型参数T上加上out关键字有2层含义:

    • 子类型化会被保留
    • T 只能用在out位置

所以当类型参数定义为了out,这个类型参数在泛型类中就不能出现在in位置,编译器会报错。

逆变:反转子类型化关系

我们清楚了协变的概念,就很容易理解逆变,逆变就是协变的反子类型化。

直接定义:一个在类型参数上逆变的类是这样一个泛型类(我们以Consumer< T >为例),对于这种类来说,如果B是A的子类型,那么Consumer< A >就是Consumer< B >的子类型。

关于逆变,我们也举个例子:

//这是迭代集和的排序函数,这里要传递进来一个Comparator,它的类型是in T
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
    if (this is Collection) {
       if (size <= 1) return this.toList()
       @Suppress("UNCHECKED_CAST")
       return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList()
    }
    return toMutableList().apply { sortWith(comparator) }
}

然后我们有以下代码:

//泛型实参为Any
val anyComparator = Comparator<Any> { o1, o2 -> o1.hashCode() - o2.hashCode() }
//泛型实参为Number
val numberComparator = Comparator<Number> { o1, o2 -> o1.toInt() - o2.toInt() }
//泛型实参为Int
val intComparator = Comparator<Int> { o1, o2 -> o1.toInt() - o2.toInt() }

//创建了Number类型的集和,按照接口,它期望的参数是Comparator<Number>
val numbers: List<Number> = arrayListOf(1,11,111)
//可以编译
numbers.sortedWith(numberComparator)
//可以编译
numbers.sortedWith(anyComparator)
//不可以编译
numbers.sortedWith(intComparator)

上面我们举了个例子,其中Number是Int的超类型,是Any的子类型,但是在传递参数却无法传递Comparator< Int >,其实我们可以想想这是为什么

因为标记为in的逆变类型参数,是需要入参的,也就是需要使用这个类的实例,比如这里期望是Number,我调用Number中的方法,但是来了一个它的子类型Int,假如Int有个getInt的方法,这时在泛型类中类型参数还是Number,它是没有getInt方法的,所以不允许。但是来了一个它的超类型Any,Any中的操作我Number都可以操作,所以是安全的。

到这里,我们把协变和逆变都说完了,其实不用死记硬背,要理解库设计的思想和原理,这个就很容易理解。

点变型: 在类型出现的地方指定变型

在开始我们就说了,变型是描述泛型类类型之间的关系,根据是否保留子类型化可以分为协变、逆变和不变。除了这些,我们还要掌握点变型的概念。

其实上面说的协变和逆变我们都是在定义泛型类或者泛型函数时使用,这样还是不太方便,比如MutableList这个类的泛型参数是不变的,但是有一些需求我们想改变一下这个定义,直接还是看个例子。

我们想编写一个复制 MutableList的函数,直接如下代码:

//参数都是mutableList
fun <T> copyData(source: MutableList<T>
                 , target: MutableList<T>){
    for (item in source) {
        target.add(item)
    }
}

然后我们在使用时:

val strings = arrayListOf("zyh","wy")
var targets: ArrayList<String> = ArrayList()
//都是String类型,当然可以
copyData(strings,targets)

但是仔细一想这不太符合逻辑,我可以定义一个String类型的集和,把它复制到Any类型的集和,这个逻辑肯定是符合逻辑的:

不出意外,这里肯定不行,因为MutableList< String >不是Mutable< Any >的子类型,按照之前的做法我们只需要把source的泛型类给改成协变类,那不免有些麻烦,我们换个方式,用下面代码:

//直接定义2个类型参数,来限制这2个类的关系
fun <T : R,R> copyData1(source: MutableList<T>
                 , target: MutableList<R>){
    for (item in source) {
        target.add(item)
    }
}

然后我们在使用时:

val strings = arrayListOf("zyh","wy")
var targets: ArrayList<Any> = ArrayList()
copyData1(strings,targets)

这样不会有任何问题,不过这太麻烦了,Kotlin和Java都有更优雅的处理方式,也就是点变型,啥是点变型呢 就比如这里我想在souce这个参数类型上它是协变的,这个在Java很常见,因为Java的所有泛型在定义时都是不变的,只有在使用时指定其变型,我们来看一下Java代码

//这里使用<? extent T>就说明可以使用T的子类型
public static <T> void copyDataJava(ArrayList<? extends T> source
        , ArrayList<T> destination){
    for (int i = 0; i < source.size(); i++) {
        destination.add(source.get(i));
    }
}

private void  testJava(){
    ArrayList<String> strings = new ArrayList<>();
    strings.add("zyh");
    strings.add("wy");
    //这里虽然类型是Object,但是不会报错
    ArrayList<Object> objects = new ArrayList<>();
    copyDataJava(strings,objects);
}

这里代码如果用Kotlin的话就更简单了:

//加了一个out变型修饰符
fun <T> copyData2(source: MutableList<out T>
                 , target: MutableList<T>){
    for (item in source) {
        target.add(item)
    }
}

直接指定source的类型类是协变的,这样就可以传递其子类型了。

这样一看就能看出来点变型是非常重要了,不仅仅可以在方法参数,也可以在局部变量、返回类型等都可以指定其点变型。

投影

看到点变型如此方便,我就有了个疑惑,MutableList这个类在定义的时候是不变型的啊,在使用时又给它设置为了协变,那是不是MutableList< T >在T上就是协变了呢 当然不是,要是这样那岂不是乱套了。

比如代码:

fun <T> copyData2(source: MutableList<out T>
                 , target: MutableList<T>){
    for (item in source) {
        target.add(item)
    }
}

这里的source其实不是一个常规的MutableList,它是受到限制的,这个叫做类型投影,即它不是真正的类型,只是一个投影

其实投影还挺好理解的,它就是受限制的类型,不是真正的类型,因为它的有些功能是受限的,是假类型。

比如上面代码,标记为了协变,那MutableList种在in位置使用的方法将无法调用,

这里add方法在MutableList中是泛型参数在in的位置,所以这里直接无法使用,功能受限,也验证了为什么叫做投影。

这个其实也非常好理解,一旦使用点变型让一个类型协变,将无法使用类型参数在in位置的方法,然后这个新的"假"类型就叫做投影。

星号投影

在理解星号投影时,还要回顾一下啥是投影,前面说了投影是受限的类型,比如在in和out都可以用的类型参数,在点变形为out时,生成的投影类型将不能使用实参在in位置的方法。

星号投影也是一种投影,它的限制是相当于out,即只能使用类型参数在out位置的方法。

直接理解完定义后,我们需要思考为什么要有这个星号投影,当类型参数不重要时可以使用星号投影,啥叫做不重要呢 就是当不调用带有类型参数的方法或者只读取数据而不关心其具体类型时。

比如我想判断一个集和是不是ArrayList时:

val strings = arrayListOf("zyh","wy")
//这里只能用星号投影
if (strings is ArrayList<*>){
    Logger.d("ArrayList")
}

因为泛型在运行时会被擦除,所以类型参数并不重要,所以这里使用星号投影。

星号投影和Any?

关于星号投影还容易理解出错,比如MutableList< * >和MutableList< Any? >这2个是不一样的东西,其中MutableList< Any? >这种列表可以包含任何类型,但是MutableList< * >是包含某种类型的列表,这个类型你不知道而已,只有当被赋值时才知道。

直接看个例子:

在被赋值前,不知道unknowns的类型,但是赋值后,可以通过get获取它的值,但是add方法无法调用,这里受到了限制,是不是有点熟悉,这个限制就是 out 点变型的协变。

所以这里星号投影被转换成了 < out Any? >,只能调用其类型参数在out位置的方法,不能调用类型参数在in位置的方法。

总结

泛型的知识还是很多的,这一篇文章也可能说不全,下面做个简单的总结:

    1. Java/Kotlin的泛型都是伪泛型,在使用时可以用在函数、扩展属性和类上,同时泛型参数在运行时会被擦除。
    2. 由于泛型参数在运行时会被擦除,所以对类型检查和类型转换函数在运行时可能会不起效果,这时IDE会给出警告。
    3. Kotlin通过内联函数和reified关键字可以实化类型参数,即在运行时还可以保留该类型参数,常常可以用于替代类引用。
    4. 加强认知一点就是类、类型和子类型的关系,对于非泛型类来说,类和类型差不多;对于泛型类来说,一个类,有多类型;而任何时候要求A类型参数时可以用B类型实例代替,B就是A的子类型。
    5. 变型就是主要讨论相同基础类型的不同类型的关系,比如Int是Number的子类型,那List< Int >类型是List< Number >的子类型,这就是协变; 同理还有逆变和不变。

阅读量:1827

点赞量:0

收藏量:0