目录
集合判断式 all , any , count , find
协变和逆变
概念:Kotlin 的协变与逆变统称为 Kotlin 的变型。变型是指泛型的基础类型与它的参数类型是如何关联的。对于普通类型来说,我们可以使用子类代替父类,因为子类包含了父类的全部内容。但是对于泛型来说,如果泛型的基础类型相同,其中一个参数类型是另外一个参数类型的子类,泛型类也不存在这种继承关系,无法直接替换使用。要解除这些限制,就需要用到协变与逆变。
先声明两个泛型接口
//未加 out
interface Production<T>{
fun product():T
}
//未加 in
interface Consumer<T>{
fun consume(item:T)
}
在声明三个类,继承关系:Burger—extend—>FastFood—extend—>Food
open class Food {}
open class FastFood :Food(){}
class Burger:FastFood(){}
接着声明食品工厂
class FoodShop : Production<Food>{
override fun product(): Food {
return Food()
}
}
class FastFoodShop : Production<FastFood>{
override fun product(): FastFood {
return FastFood()
}
}
协变( OUT )
main函数创建这两个食品工厂
fun main() {
//协变
var foodshop:Production<Food> = FoodShop()//正常编译
var fastfoodshop:Production<Food> = FastFoodShop() //报错
}
此时第4行代码相当于 Production<Food>类型引用指向Production<FastFood>类型对象。
由于Production<Food>并不是Production<FastFood>的父类,相互之间无继承关系,所以编译器并不能匹配正确的引用类型。
Java为了解决这个问题提供了<? Extends T>通配符,而在Kotlin中将这个通配符用out关键字来替代。需要在Production接口泛型T前加out关键字,表明泛型可以传入参数类及其子类
//加 out 编辑通过
interface Production<out T>{
fun product():T
}
不是所有类都可以变成协变的
概念:只有当这个类只能生产类型 T 的值而不能消费它们时,才能变成协变的。就是说 T 的值只能作为函数返回值时,才能变成协变的。这也是为什么协变关键词叫做 out,表明它只能作为生产者对外输出。
以下代码中,t : T 接收入参的位置叫做 in 位置,表示它是函数参数,是消费者。: T 返回值输出数据 的位置叫做 out 位置,表示它是函数返回值,是生产者。
Kotlin |
举个例子:
//Apple和Orange都是Fruits的子类
open class Fruits(val name: String) {}
class Apple(val n: String, val male: String = "apple"): Fruits(n) {}
class Orange(val n: String, val female: String = "orange"): Fruits(n) {}
//先声明个泛型类,内部封装了一个私有的data属性并向外部提供set、get方法
class MyClass<T> {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? = data
}
fun test(data:MyClass<Fruits>) {
data.set(Orange("橙子"))
}
定义测试主函数
可以看到test传入参数爆红,这就和最开始说的Production<Food>并不是Production<FastFood>的父类是同样的问题,如果说kotlin允许这样跨继承传参(即test方法调用时不编译报错),那data.get()拿到的就是一个Orange类型的对象而data.get()需要返回的是一个Apple类型的对象,这样就会发生类型转换异常。所以kotlin是不允许这样去跨继承传参的
而换个角度想之所以这样写会出现类转换异常就是因为test方法中给set了一个Orange对象导致了问题,如果说MyClass在泛型T上是只读的即没有set方法那么就不会因为set一个Orange对象导致类型转换异常。
所以kotlin规定如果个泛型类或泛型接口定义out协变后,泛型T只能出现在out位置不能出现在in位置,即只能读不能写
逆变 ( IN )
逆变和协变是相反的,但其实道理是一样的
之所以第一句编译报错就是因为 ArrayList<Apple>并不是 ArrayList<Fruits>的子类无法进行类型强转。同样的,java为了解决这个问题提供了<? super T>通配符,而在kotlin中将这个通配符用in关键字来替代,in修饰Apple表明pl这个集合对象中存的可以是Apple及其父类的对象:
同样的定义个泛型接口
interface MyClass<T> {
fun show(d: T?): T?
}
fun testTwo(data:MyClassTwo<Apple>){
var result = data.show(Apple("apple"))
}
定义测试主函数
可以看到调用时编译报错了,还是因为kotlin不支持直接跨继承传参。如果说编译不报错,那么继续走下去会看到形参data是Apple类型的MyClassTwo引用,result变量要求接收一个Apple类型实现的MyClassTwo对象,但是实际上实参data的show方法返回了一个Orange对象,由于它是Fruits的子类,所以data在实现show方法的时候并没有问题,但是result在接收的时候无法将Apple强转为Orange类型,这样就会发生类型转换异常。和协变一样
我们换个角度想之所以这样写会出现类转换异常就是因为show方法要去返回一个Fruits对象导致了问题,如果说MyClassTwo在泛型T上是只写的即不允许泛型T出现在out位置上,那么就不会因为show方法返回了一个Orange对象而导致的类型转换问题。
kotlin为了实现这个只能写不能读的功能而提供了in关键字,即这个泛型T只能出现在in位置不能出现在out位置
协变和逆变总结
总的来说协变和逆变是java为了处理泛型的类型擦除而带入的新规则,kotlin在java的基础上用了out和in两个关键字来实现
out和in使用规则:
reified关键字
修饰内联函数( inline )的泛型,泛型被修饰后,在方法体里,能从泛型拿到泛型的Class对象
有时候,可能想知道某个泛型参数具体是什么类型,reified关键字能帮助检查泛型参数类型,Kotlin不允许对泛型参数T做类型检查,因为泛型参数类型会被类型擦除,就是说,T的类型信息在运行时是不可知的,Java也有这样的规则
//boy和man是Human的子类,都实现了toString方法
open class Human(val age: Int)
class Boy(var name: String,age:Int):Human(age){
override fun toString(): String {
return "Boy(name='$name')"
}
}
class Man(var name: String,age:Int):Human(age){
override fun toString(): String {
return "Man(name='$name',age=${age})"
}
}
class ReifiedTest<T:Human> {
//reified修饰内联函数inline泛型T
inline fun <reified T> randomOrBackup(backop:()->T):T{
val items= listOf(
Boy("Jack",20),
Man("John",35)
)
val random = items.shuffled().first()
println(random)
//判断如果随机返回的类型和泛型类型一致则返回随机类型,否则调用backup()
return if (random is T){
random
}else{
backop()
}
}
}
fun main() {
val test= ReifiedTest<Human>()
//由backup函数,推断出来T的类型
val result = test.randomOrBackup {
Boy("李磊",55)
}
println(result)
}
扩展函数
扩展可以在不直接修改类定义的情况下增加类的功能,扩展可以用于自定义类,也可以用于比如List,String,以及Kotlin标准库里的其他类。和继承相似,扩展也能共享类行为,在你无法接触某个类定义,或者某个类没有使用open修饰符,导致你无法继承他时,扩展就是增加类功能的最好选择
使用 Kotlin 的扩展函数还有一个好处就是没有副作用,不会对原有库代码或功能产生影响。
定义一个扩展函数
定义扩展函数只需要把扩展的类或者接口名称,放到即将要添加的函数名前面。这个类或者名称就叫做接收者类型,类的名称与函数之间用 . 调用连接。this指代的就是接收者对象,它可以访问扩展的这个类可访问的函数或属性。
//给字符串追加自身长度个!
fun String.ExtendFun() :String{
return this+"!".repeat(this.length)
}
fun main() {
println("abc".ExtendFun()) //字符串abc为接收者对象
原理
扩展函数实际上就是一个对应 Java 中的静态函数,这个静态函数参数为接收者类型的对象,然后利用这个对象就可以访问这个类中的成员属性和方法了,并且最后返回一个这个接收者类型对象本身。这样在外部感觉和使用类的成员函数是一样的:
如图扩展函数ExtendFun对应实际上是Java中的静态函数,并且传入一个接收者类型对象作为参数,操作完成后,最终返回接收者对象自身
扩展函数需要的注意点
- 扩展函数不能像成员函数那样被子类重写
- 扩展函数不能访问原始类中私有成员属性或成员函数
- 扩展函数的本质通过传入对象实例,委托对象来访问成员函数,所以它的访问权限和对象访问权限一致。
扩展函数使用场景
虽然扩展函数功能十分强大,但是千万不要滥用扩展函数,并不是所有场景定义都合适定义成扩展函数
|
可空类型扩展
定义扩展函数用于可空类型,这样可以直接在扩展函数体内解决空值问题
格式: 接收者类型?. 函数名(){}
fun String?.printOrDefault(default:String){
println(this?:default) //如果接收者对象为null 则打印默认值
}
fun main() {
var nullString:String? = null
nullString.printOrDefault("默认值")
}
泛型扩展函数
泛型扩展函数可以支持任何类型的接收者,还保留了接收者的类型信息
fun <T> T.easyPrint():T{
println(this)
return this
}
fun main() {
"abc".ExtendFun().easyPrint() //任何类型都可调用
}
标准函数与泛型扩展函数
标准函数本身就是泛型扩展函数,所以它可以让任何类型调用,如let源码:
applay是如何实现隐式调用的
扩展属性
除了给类添加功能扩展外,还可以给类定义扩展属性
val String.numVowels
get() = count { "aeiou".contains(it) } //统计元音字母个数
函数式编程
函数式编程主要依赖于高阶函数(以函数为参数或者返回函数)返回的数据,这些高阶函数专用于处理各种集合,可方便地联合多个同类函数构建链式操作以创建复杂的计算行为,Kotlin支持多种编程范式
一个函数式应用通常用三大类函数构成,变换transform,过滤Filter,合并combine。每类函数都针对集合数据类型设计,目标是产生一个最终结果。函数式编程用到的函数生来都是可组合的,也就是说,可以组合多种简单函数来构建复杂的计算行为
map函数
map函数对集合中的每一个元素应用给定的函数并把结果收集到一个新函数。
fun main() {
var listOf = listOf("a", "b", "c")
var maplist = listOf.map {
"${it} say: Hellow"
}
println(maplist)
}
//结果:[a say: Hellow, b say: Hellow, c say: Hellow]
filter函数
filter函数遍历集合并选出满足判断式的元素。
fun main() {
var listOf = listOf(1, 2, 3)
var filter = listOf.filter { it % 2 == 0 }
println(filter)
}
//结果 [2]
集合判断式 all , any , count , find
fun main() {
val list = listOf(1,2,3,4,5,7,8)
val canBeInClub = { i : Int -> i%2==0}
//all:检查所有元素是否都满足判别式(Boolean)
println(list.all(canBeInClub))
//any:检查集合中是否至少存在一个匹配的元素(Boolean)
println(list.any(canBeInClub))
//count:有多少个子元素满足判断式(Int)
println(list.count(canBeInClub))
//find:找到一个满足判断式的元素(T)
println(list.find(canBeInClub))
}
groupBy
通过这个函数可以把列表按不同特征划分为不同的分组(返回值类型:Map<K, List>)
val people = listOf(Person("Alice",29) , Person("aaa",18),
Person("ccc",29))
val map = people.groupBy { it.age }
println(map)
//结果:{29=[Person(name=Alice, age=29),
// Person(name=ccc, age=29)], 18=[Person(name=aaa, age=18)]}
flatMap 和 flatten: 处理嵌套集合中的元素
flatMap:
它可以操作一个集合的集合,将其中多个集合中的元素合并后返回一个包含所有元素的单一集合
//根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并成一个列表。
val strings = listOf("abc","def")
//字符串的toList函数把它转换为字符列表
println(strings.flatMap { it.toList() })
//结果:[a, b, c, d, e, f]
flatten:
根据上述官方的定义,flatten函数的功能其实就是“展开”,即将嵌套的Collection,展开为最底层的元素。官方示例如下:
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten())
// return [1, 2, 3, 4, 5, 6, 1, 2]
序列:惰性操作集合
概念:List,Set,Map集合类型,这几个集合类型统称为及早集合,这些集合的任何一个实例在创建后,它要包含的元素都会被加入并允许你访问。对应及早集合,kotlin还有另外一类集合:惰性集合类似于类的惰性初始化,惰性集合类型的性能表现优异,尤其是用于包含大量元素的集合时,因为集合元素是按需产生的
序列不会索引排序它的内容,也不记录元素数目,使用一个序列时,序列里的值可能有无限多,因为某个数据源能产生无限多个元素
创建序列
generateSequence函数:给定序列中的前一个元素,这个函数会计算出下一个元素。
序列使用场景:例如找前1000个素数
先定义一个找素数的扩展函数
fun Int.isPrime():Boolean{
(2 until this).map {
if (this%it==0){
return false
}
}
return true
}
直接使用集合:因为无法确定第一千个素数在哪个区间内,所以只能试,或者用while循环,以(1..5000)为例子
fun main() {
//假定5000之内有一千个素数
var list = (1..5000).toList().filter { it.isPrime() }.take(1000)
println(list.size)
}
//结果 670并不够1000 低效
使用序列,不用找区间
fun main() {
println(
generateSequence(2){ it+1 }
.filter { it.isPrime() } //调用过滤器传入isPrime扩展函数
.take(1000) //其中take决定序列迭代函数执行了多少次
.toList()
.size
)
}
//结果 1000
可以调用拓展函数asSequence把任意集合转换为序列。调用toList来做反向的变换。 |
Java和Kotlin互操作注解
@JvmName
这个注解的主要用途就是告诉编译器生成的Java类或者方法的名称
//给文件加别名
@file:JvmName("Hero")
package com.derry.s8
//给方法加别名
@JvmName("fun1")
fun aaa(){
println("aaa")
}
Java调用
@JvmField
在属性上添加,使Kotlin编译器不再对该字段生成getter/setter并将其作为public公开字段,Java互操作时可直接调用
@JvmOverloads
这个注解解决了Java不能重载Kotlin有默认参数的方法
比如Kotlin代码如下调用是没有问题的
class TestKt {
fun testJvm(a: String, b: Int = 1) {}
fun abc() {
testJvm("a")
testJvm("a", 3)
}
}
但是Java如果进行调用的话
class TestJava {
private void tt() {
TestKt test = new TestKt();
test.testJvm("3"); //这里会报错
test.testJvm("3", 4);
}
}
这时候就需要我们使用@JvmOverloads进行一个适配(这样就没问题了)
class TestKt {
@JvmOverloads //添加注解
fun testJvm(a: String, b: Int = 1) {}
}
@JvmStatic
解决在Java中不能直接调用kotlin 中的静态方法
在Kotlin中可以直接通过类名.()方式调用companion静态代码块中的方法
class TestKt {
companion object {
fun abc() {}
}
fun test(){
TestKt.abc()
}
}
而Java中调用需要增加.Companion调用
class TestJava {
private void tt() {
TestKt.Companion.abc(); //Java中调用
}
}
如果想在java中也直接类名.调用静态方法需要在相关方法上加入@JvmStati
class TestKt {
companion object {
@JvmStatic //修饰方法
fun abc() {}
}
}
@JvmSynthetic
如果你写的⼀个函数你只想给kotlin代码调⽤ ⽽不想给java的代码调⽤ 那你就在你的函数上⾯加上这个注解即可