入行没几年的小码农,近期学习Kotlin,做一份笔记记录,此文依据《Kotlin实战》这本书的流程记录,部分示例内容均摘自《Kotlin实战》,记下自己的理解,本篇记录在Kotlin中泛型的相关使用方式。
Kotlin学习笔记系列
新手上路,Kotlin学习笔记(一)-- Kotlin入门介绍
新手上路,Kotlin学习笔记(四)---Lambda表达式在Kotlin中的使用
新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统
新手上路,Kotlin学习笔记(六)---运算符重载和其它约定
新手上路,Kotlin学习笔记(七)---Lambda作为形参和返回值的使用
一、Kotlin中泛型的介绍
在Java中,对于泛型我们应该很熟悉了,使用最频繁的就是在集合中使用泛型。
我们先介绍一下:泛型允许你定义带类型形参的类型,当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。
通过上面介绍,我们可以理解为,我们看到的List<T>这里的T就是类型形参,当创建一个实际的List时,比如List<String>,这里的String就是类型实参。所以,类型形参只是声明使用的,实际使用的时候还是类型实参!
在声明泛型的时候,和Java的使用方式基本一致,使用尖括号即可,当一个类继承泛型类或实现一个泛型接口,需要显式的为其指定一个类型实参(可以是一个具体的类型或者另一个类型形参)。
二、类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
如果把一个类型指定为泛型类型形参的上界约束,那么在泛型类型具体的初始化中,其对应的类型实参必须是这个具体类型或者子类型。
可以理解为,我们为这个泛型指定了一个上限(父类),其最大的范围只能是这个类,在Java中我们会用extends来表示一个泛型的范围,如List<T extends People>,在kotlin中的对应表示就是List<T : People>,此时这个List中的实例只能是People的子类或者People类型本身。并且可以直接调用这个上界的方法,如:
fun <T : Comparable<T>> max (first : T , second : T) : T
{
return if (first > second) first else second
}
当我们声明其上界为Comparable的时候,我们可以直接调用Comparable才有的重载算术运算符方法 >
在Kotlin中,我们还可以为一个泛型定义多个约束,这样传入的类型实参将需要同时实现多个接口,如下面示例:
fun <T> ensureTrailingPeriod(seq : T) where T : CharSequence, T : Appendable
{
if(!seq.endsWith("."))
{
seq.append(".")
}
}
用where关键字来限定泛型的多个上界,这样说明作为类型实参传入的参数一定实现了CharSequence和Appendable接口,我们就可以同时调用这两种接口的方法了。
tips:对于没有显示声明泛型上界的情况,其会拥有默认的上界Any?,所以我们在使用的时候是需要做非空判断的,如果不希望其为空,应显式的声明为Any。
三、运行时的泛型
1、泛型的类型检查和转换
我们知道,在JVM上,对于泛型,是通过类型擦除实现的,就是泛型类实例的类型实参在运行时时不可保留的。比如一个List<String>,在运行时只能看到是一个List,并不能识别出包含的是哪种元素。所以我们不能在is检查中使用类型实参中的类型,下面代码将不能被编译:
当然,JVM擦除泛型类型信息是有好处的,要保存在内存中的类型信息会更少,所以应用程序的内存总量会少一些。
因为Kotlin不允许使用没有指定类型实参的泛型类型,此时又不能使用类型实参,那么当我们需要只需要判断一个集合是List或者Set该怎么办呢?
此时可以使用星号投影,如下所示:
fun <T : Person> checkPerson(p : Collection<T>)
{
if (p is List<*>)
{
//这样写是被允许的,此处判断了p是一个List
}
}
上面是is使用的场景,当使用关键字as的时候,编译器将允许我们书写一般的泛型类型的转换,但是如果该类有正确的基础类型,但是类型实参是错误的,转换不会报错,还是因为在运行时类型实参是未知的,当我们再次使用的时候,将会抛出异常!
fun printSum(c : Collection<*>)
{
val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
print(intList.sum())
}
此处编译器会提示警告,但是允许我们这样书写。
上述代码,在执行的时候,如果传入的不是List,将会是我们设想的弹出IllegalArgumentException,如果传入的是一个List<String>,intList的类型转换将是成功的,而在后续进行sum操作的时候,报错ClassCastException。
tips;对于已知的类型实参的集合,编译器是足够智能的,所以我们可以使用is进行转换,如:
fun checkIs(c : Collection<Int>)
{
if(c is List<Int>)
{
//这里的判断是允许的,因为传进来的一定是Int类型实参的集合,运行时不知道类型实参也不影响该判断
}
}
2、声明带实化类型参数的函数
当一个函数是泛型函数的时候,我们刚刚知道了,其在运行的时候会被擦出类型实参,所以我们就无法获知这个类型实参是什么,下面的代码将无法编译:
针对这种情况,Kotlin为我们提供了一种解决方式,那就是使用内联函数,上一篇博客我们已经知道了,使用inline标记的内联函数,将会在调用的时候替换为函数实际代码的实现,所以我们将上述函数声明为inline并用reified标记类型参数,就可以检查value是不是T的实例。
inline fun <reified T> isA(value : Any) : Boolean
{
return value is T
}
四、变型:泛型和子类型化
前面我们在介绍泛型的参数约束时提到,在泛型类型的具体初始化时,必须是这个类型或者其子类型。我们说的是子类型,而不是子类!子类型和子类是有区别的!
考虑下面一个场景,此时有这样一个方法:
fun addAnswer(list : MutableList<Any>)
{
list.add(42)
}
如果我们传入一个类型为List<String>的对象进去,代码不就会报错了吗?所以子类型并不是单纯的子类。
子类型:任何时候如果需要的是类型A的值,你都能够使用类型B的值(当作A的值),类型B就称为类型A的子类型。
根据定义,我们就知道了Int是Number的子类型,是Int?的子类型,同样的,Int也是Int本身的子类型。
1、协变
一个协变类是一个泛型类(我们以Product<T>为例),如果A是B的子类型,那么Product<A>就是Product<B>的子类型。
Kotlin中要声明一个类在某个参数上是可协变的,在该类型参数前面加上out关键字即可。如List这个类
public interface List<out E> : Collection<E>
之前我们学过,List在kotlin中是只读的,只会在返回值处使用泛型,所以在不会出现MutableList这种被改变数据后出现异常的场景。此时List是协变的没有任何问题。
2、逆变
我们来看Comparable接口,该接口只在方法内部使用了泛型,并没有像List那样在返回值处使用。
/**
* Classes which inherit from this interface have a defined total ordering between their instances.
*/
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
此时,该接口的泛型声明处加了关键字in,并且我们也知道,一个要求比较Any的对象,不能传入Comparable<String>。但是一个String的比较,既可以传入Comparable<String>,又可以传入Comparable<Any>,这与之前的协变刚好相反,这种情况称作逆变。
3、不变性
相对于协变和逆变,MutableList这种类,即将泛型当做返回值,又作为参数进行修改使用,我们称之为不变型
同时,也存在协变和逆变并存的场景,在一个类中,一个协变,一个逆变,如Function1这个类:
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
P1这个类型用在了方法的参数中,R则使用在返回值处。
4、点变型
在一个函数中,如果有多个参数都使用同一个泛型,但是某个参数实际只使用了协变或者逆变的场景,同样可以使用in或者out进行修饰,这种方式成为点变型,如:
fun <T> copyData(source : MutableList<out T>, destination : MutableList<in T>)
{
for(item in source)
{
destination.add(item)
}
}
参数source只调用了读取的函数,destination只调用了写入的函数,所以可以对它们单独的声明协变和逆变,并且在使用的时候只允许调用相对应的函数,否则编辑器将报错。
5、星号投影
前面我们介绍了使用星号*的场景,其实,MutableList<*>星号投影就相当于MutableList<out Any?>的场景,此时可以读取但是不行有任何修改的实现。
tips:最后,我们所进行依据的逆变判定是针外提供的方法的,对于私有方法,其一个对象内部一定是同一个类型,所以如果对泛型有修改的操作都是私有方法,那么这个泛型类还是可以声明为协变的!
本章的内容就到这里了,下一章我们将学习kotlin中的注解和反射。
//下一章 新手上路,Kotlin学习笔记(九)---注解和反射