变量声明
-
val
—声明只读变量 var
—声明可读写变量
在kotlin中 val
声明的是只读变量,但是不是常量,这个说法比较有意思,和java有区别,比如,val声明一个变量,可以定义它的get方法:
它并不是一个常量,要定义一个真正意义上的常量,必须使用const
, const
只能修饰基本类型,且必须初始化
这样定义了一个编译时常量,等价于java的 static final int
val
跟java的final
一样,可以不指定初始值,但是必须要在后面的某个地方初始化它:
建议:尽可能使用 val 来声明不可变引用,让程序的含义更加清晰稳定。
kotlin基本数据类型
- 变量声明:
特点是冒号后面跟上类型
- 变量声明—类型自动推导:
这种方式可以省略类型,由kotlin自动推导
- 长整型写法,必须以大写 L 结尾,与java不同:
- Float是后面加小写的
f
, 不写f
的小数就是Double类型的:
- 与java不同,kotlin中所有类型转换必须显示调用方法:
- 字符串支持模板变量引用:
===
比较引用,==
比较值:
集合相关
集合类型
集合框架的创建
- 只有可变的才能添加删除元素:
- 集合遍历:
- 集合遍历可以直接通过
+
或-
号来添加/删除元素:
- 与java不同,可以直接通过
[ ]
下标索引赋值取值:
- map也可以直接用
[ ]
来存取:
- 特殊集合类型:Pair表示一对值, Triple表示一个三值集合
数组类型
kotlin中的数组类型跟java相对应,其中基本类型和装箱类型的写法也是不一样的,装箱类型的都带有泛型参数,基本类型直接是一个类
数组的创建
- 数组的长度:
比java好的地方:不需要.length和.size区分了,集合长度也是.size
- 引用数组值
- 数组遍历:
显然forEach
的遍历方式最方便
- 判断某个元素是否在数组中:
- 判断某个元素不在数组中:
区间类型
- kotlin中的区间类型,java没有
- 开区间
- 倒序区间
- 区间步长
- 可数区间 即离散区间才能打印出来,连续区间是不能打印的
- 可数的离散区间才支持步长设置:
- 遍历离散区间跟数组一样:
- 判断一个值是否在连续区间中:
- 判断一个值是否在离散区间中:
- kotlin中的
for-i
循环便捷写法:
集合、数组、区间这三种类型他们的遍历和判断是否在集合中都是一样的方法,使用for-in语法。
函数定义
其中函数返回值为Unit
可以省略 即void
类型省略,跟java也是一样的。
- 函数的引用
函数引用的写法感觉比较奇怪,它是函数名前面加两个冒号,函数的引用类似C语言中的函数指针,可用于函数传递。
左边冒号后面的函数类型可以写,也可以省略掉,简写:
其中等号右侧冒号前面有类名的是类对象的方法引用,在调用的时候也要传对象实例才行:
- 变长参数:kotlin中函数变长参数类型使用
vararg
修饰
这时ints实际上是一个IntArray,可以进行遍历操作等。这个变长参数类型很奇怪,看上去是var和arg两个单词的合并。
- 函数默认参数:
- 如果默认参数不是最后一个,必须使用具名参数:
- 函数参数可以是另一个函数:
- 多参数返回值:
实际就是返回了一个Triple对象,它可以持有多个值而已
表达式
- 在kotlin里面分支判断都是表达式而不是语句,在java中是语句,这个是kotlin与java的最大不同
由于是表达式,所以分支判断都可以直接赋值为一个变量,c = if (a == 3) 4 else 5
,因此kotlin里面没有三目运算符,是因为本身表达式就支持。类似的when
、try-catch
全部可以赋值
这样可以直接赋值确实比java方便了许多!when
代替了java的switch-case
,写法也更简洁了
运算符重载
kotlin支持运算符重载,类似C++,kotlin中的 ==
、+
、>
、[]
、包括函数调用符号 ()
都是kotlin中内置好的重载运算符,参考:https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators 在IDE当中点击对应的符号可以直接跳转到对应的实现。
运算符重载例子:复数运算操作
运算符重载例子2:实现字符串的加减乘除
重载运算符的定义特点就是类定义扩展方法,方法名和运算符对应的描述,可以到前面的网址上查,然后方法前面加 operator
关键字。
定义hascode和euqals方法:
与java一样,如果是添加到哈希一类的数据结构中,必须重写equals和hascode方法,如果equals方法判断相同,则认为是同一个对象。
lambda表达式
- kotlin里面的lambdas表达式是一个匿名函数的语法糖,因此它的类型其实就是对应的函数类型
- java中的lambdas表达式其实是接口类型的一个语法糖,二者不同
kotlin中lambda表达式中最后一行的类型就是函数的返回类型
中缀表达式
kotlin中比较奇怪的一种写法
本质还是类的扩展方法,前面加 infix
关键字,可能是为了实现更加语义化的书写方式
高阶函数
高阶函数简单来说就是函数的参数可传递另一个函数,常见于forEach
表达式:
例子:定义一个函数打印方法的耗时
调用:
显然在java中这样是做不到的,必须每个方法里面前后去打印一下才行。。
内联函数
添加inline
关键字的函数标记为内联函数,内联函数的特点是,代码会被直接插入到调用处,编译的代码反编译结果就是代码直接插入到调用处的效果。内联函数效率高一些。
forEach在kotlin中的实现就是内联函数:
高阶函数搭配内联函数使用更加高效:
编译后等价于:
内联函数的返回:
其实是跳过3并不是返回,等价于:
- nonLocalReturn 返回调用的方法,下面直接返回main方法
- 禁止non-local-return,使用
crossinline
关键字
- 内联属性:
内联函数的限制:
- 内联函数只能访问对应类的 public 成员
- 内联函数的参数不能被存储(赋值给变量)
- 内联函数的参数只能传递给其他内联函数参数
扩展函数
标准库中的常用扩展函数:let、run、also、apply、use
在IDE中点击打开 use
方法的实现源码:
可以发现它基本上实现了我们java的try-catch-finally
结构,包括最终流它也默认帮你关闭了,你都不需要关心了,可以说用起来真的省事多了!IO读写再也不用那么麻烦了,比java又臭又长的写法真是方便太多!
集合变换操作符
集合的变换操作
- sequence的概念
sequence类似java8里面的stream流,或者RxJava里面的流概念
上面代码list
调用asSequence
后每个元素会依次调用filter
和map
, 不加asSequence
每个元素会先调用filter
再调用map
。
加asSequence
最后不加forEach
的话,不会有输出,不加asSequence
的话,去掉forEach
也会有输出。即加上asSequence
变成了流一样。
asSequence
被称为懒序列,使用asSequence
性能会优化一些,因为每个元素只需要走一遍每个操作,而不是每个操作中将每个元素走一遍。
集合的聚合操作
sum
reduce
fold和reduce有点递归的意思在里面,每次的结果都是基于上次的结果。
zip
看源码,zip其实就是将两个集合遍历执行某个操作,只不过最终集合大小是以最小长度的那个集合为准:
集合变换应用例子:
统计文本文件中非空格字符出现的次数
SAM转换
- Java 的 SAM:
- Kotlin 的 SAM:
- Kotlin的匿名内部类:
kotlin中SAM目前只支持只有一个方法的java接口
kotlin中SAM不支持只有一个方法的kotlin接口, 但是可以直接定义一个函数参数
下面这样写法是不行的:
下面这样写法是可行的:
- SAM转换支持对比:
一个例子,添加和移除监听的正确kotlin写法:
使用上面的java类:
DSL: 领域特定语言
如sql语言、gradle中的groovy语言等,kotlin可以方便的实现这些语言的写法
例子: 通过拼接操作生成一个html文件
这个例子主要有两点:
- 一个是如果是变量后面跟东西相当于传递一个lambda表达式,那定义的时候其实就是定义一个高阶函数来实现;
- 二是如果字符串后面跟东西相当于运算符重载
invoke
,跟{}
相当于参数是一个ambda表达式,跟()
就是普通参数,定义String类的扩展函数即可实现。
+"Hello HTML DSL!!"
这种也是字符串的运算符重载:
字符串前面后面跟操作符好像基本都是运算符重载
另外扩展方法中如果想访问除了自身以外的其他Receiver的话,只需将扩展方法定义到对应的类内部即可,如上面的String相关扩展方法直接定义到了BlockNode类的内部,就可以引用BlockNode类的成员属性来使用了。
Kotlin中的几个特殊符号 ( ‘?.‘ ‘?:‘ ‘!!‘ ‘as?‘ ‘?‘ ) 含义
?.
安全调用符
?:
as?
!!
?
类构造器
init 块
- 构造器内省写var的属性,可以防止类内初始化,类似java中的大括号初始化,该初始化会跟构造函数一起执行。
-
init
块中可以直接访问到构造方法中的参数。 -
init
块可以有多个,可以分开写,init
块最终会合并执行。
副构造函数,后面可以指定调用主构造函数或其他副构造函数,类似java的构造函数重载,但是kotlin不推荐定义很多副构造函数(会增加复杂度),还是推荐定义一个主构造器。
- 主构造器默认参数:
- 构造同名的工厂函数,函数名也可以与类的名字不同,工厂方法比构造函数更具有表现力,因为它可以通过名字知道类是如何构造出来的
类似String有常见的工厂方法调用:
可以自己给系统String类添加工厂方法,一般写成函数名跟类名一样
其实就是一个函数返回类型为对应的类
类的可见性
- 与java不同,kotlin中啥也不写,默认就是
public
的,而java中不写默认是default
包内可见 - kotlin中多一个限制可见性的
internal
关键字,去掉了default
关键字 - 对于
protected
, java是包内可见,而kotlin是类内可见,这点不同,当然子类肯定都是可见的,kotlin中protected
只能用来修饰类的成员,不能用来修饰类和顶级声明
internal VS default
internal
这个关键字比较有意思,可以在kotlin中用作模块化隔离可见性
比如在一个模块中声明:
而在依赖它的模块中使用它会报错:
但是通过java代码却可以使用,此时可以通过internal
方法上添加@JvmName("xxx")
注解指定一个非法的java变量可达到java不能调用的目的,但实际上java是能看到的,只不过不能打出来合法的方法名而已。
构造器的可见性
属性的可见性
- get可见性必须和属性可见性一致, 不能定义public属性的get为private
- set可见性不能大于属性的可见性, 不能定义private属性的set为public
顶级声明的可见性
- 顶级声明指文件内直接定义的属性、函数、类等
- 顶级声明不支持 protected
- 顶级声明被 private 修饰表示文件内可见
延迟初始化方案
lateinit 的注意事项:
- lateinit 会让编译器忽略变量的初始化,不支持 Int 等基本数据类型
- 开发者必须能够在完全确定变量值的生命周期下使用 lateinit
- 不要在复杂的逻辑中使用lateinit,它只会让你的代码更加脆弱
使用 lazy:
lazy
是比较推荐的延迟初始化方式,实际上它是一个属性代理
接口代理
接口代理其实就是可以把一些没必要实现的接口方法隐藏起来不去实现,方便一些,而不用每一个接口都要写一下。其中by
关键字右边的就是实际代理类对象,它是构造函数中的一个属性,by
关键字左边的是代理类对象实现的接口。
例子:
属性代理 - lazy
lazy
属性代理 代理了Person实例的firstName的getter
方法,实际是一个函数 传递一个lambda表达式
observable代理属性,监听set值变化:
自定义代理属性:
其中getValue和setValue方法的参数写法是固定的
调用:
自定义实例:读取Config.properties中的配置项
Config.properties文件中一般是key-value的配置
其实主要还是实现getValue
和setValue
方法就可以,需要注意的是签名写法,setValue
最后一个参数是设置的值的类型,而getValue
的返回值就是对应获取的值的类型。
只要定义好了getValue
和setValue
方法,然后就可以通过by
操作符去代理了
使用形式:var
变量 by
【实现getValue
和setValue
的类】的实例()
Kotlin单例
只需要类前面添加object
关键字即可,object
定义类等价于java的恶汉式的单例模式
object不能定义构造函数,但可以定义init
块
使用:
@JvmStatic
和 @JvmField
- 静态成员
@JvmStatic
object
修饰的类内部方法相当于静态方法,但是伪静态,也就是内部会生成一个静态的成员对象,对类的方法调用实际上是调用的内部静态成员对象的方法,只有在方法上添加@JvmStatic
才会真正的生成静态方法。
- 不生成 gettter 和 setter
@JvmField
普通kotlin类(非单例)中使用使用JvmField
和JvmStatic
:
注意,加的注解@JvmField
和@JvmStatic
只针对java平台的可用,其他平台并不可行。
单例的object类仍可以继承类:
内部类
在kotlin
中类内部的class
前面不写修饰符默认就是 静态内部类
,class前面写 inner
修饰符才是java中的 普通内部类
,与java一样,普通内部类会持有外部类的对象引用。
object类内部的object类默认是静态的 不存在非静态的情况 不能定义成inner
匿名内部类
其实就是object 省略类名直接实现接口。
匿名内部类 可用继承多个接口,java不行:
实现多个接口时,它的类型是多个接口类型的混合类型。
Local class :
可以对比java的local class实现,其实就是在静态函数的内部定义一个类:
说实话,写了这么多年代码,未曾这么干过。。
数据类
kotlin中提供一个data
关键字,data
修饰的类就是一个数据类,对标java的bean类:
与java的bean类相比,kotlin的data类不能被继承,并且属性要全部写到构造函数当中,没有无参的构造函数。确实简便了许多!
并且编译器会为data类生成了一些好用的方法:
其中copy()
和 component1()
等都是编译器自动生成的,component
方法的意义是方便解构赋值的使用:
关于解构:
自己实现解构方法:
Kotlin的数据类内部其实会生成上面的三个component
方法:
例如上面代码翻译后就会看到:
data 不能被继承,那么为啥不能有子类呢?
可以先看一下kotlin为data类生成的对应的java类是什么样的,查看方式,doule shift键,然后Actions中输入kotlin Bytecode显示:
点击Decompile即可查看对应生成的java代码
以下是Book的数据类对应生成的java代码
除了类名方法名前面添加了final
关键字不允许继承以外,还生成了许多方法,其中有重写hashCode()
和equals()
方法,所以如果有一个类继承了data类,可能导致属性变化,从而导致hashCode()
和equals()
方法的结果不一致。
如果有子类的话,会发生什么?
如何合理的使用 data class :
data类的属性最好全部为基本类型或者其他data类型,保持它的纯净性。
另外,有方法可以破除data
类的不可继承性,也有网友在吐槽kotlin的这个设计,觉得它不好,其实如果你想用一个可以继承到类,只需要把data
关键字去掉,建一个普通类就好了。kotlin这样设计肯定是想保持它的纯洁性,如果可继承,只会变的更复杂。
枚举类
kotlin里面的枚举类跟java差不多
枚举类不可继承其他类,因为枚举类有父类是enum
,但是可以实现接口:
枚举类可以定义扩展函数:
密封类
- 密封类是一种特殊的抽象类
- 密封类的子类必须定义在与自身相同的文件中
- 密封类的子类的个数是有限的
其实就是一个只能在同一个文件中定义子类的抽象类。定义方式是在类的前面加sealed
关键字
完整的示例:控制播放器播放状态的例子
注意其中的when
表达式的使用,可见它可以用来替代枚举类做类似的功能,子类的个数也是全部可枚举的。跟枚举类有相似之处。
密封类 VS 枚举类:
内联类
内联类的概念:
- 内联类是对某一个类型的包装
- 内联类是类似于 Java 装箱类型的一种类型
- 内联类编译器会尽可能使用被包装的类型进行优化
内联类的限制:
- 主构造器必须有且仅有一个只读属性
- 不能定义有 backing-field 的其他属性
- 被包装类型不能是泛型类型
- 不能继承父类也不能被继承
简而言之,内联类是一种类型的包装类,类前面加inline
关键字,构造器只能有一个参数,不能继承类,不能被继承,不能作为内部类。类似于java的Integer、Double这种,但有不同。
内联类虽然不能继承类或被继承,但是可以实现接口。
BoxInt会做编译优化,只有在需要的时候才使用包装类 多数情况下直接使用Int
,可以反编译字节码生成的java代码查看。另外,还有个特点就是它只能有方法不能有属性。
内联类也可以用来模拟枚举类,与枚举相比内存占用小,但使用方式类似
typealias VS inline class:
Json解析
Kotlin中解析Json有那么几种方式:
- Gson
- Moshi
- kotlinx.serialization
其中Gson是原来经常使用的Google的解析库,用这个在kotlin中使用已经不适合,会有很多情况不支持,如空类型 默认值等。然后就是moshi,这个是square出品的,还有就是Kotlin自带的kotlinx.serialization,不过kotlin自带的居然不支持java类。显然对于android开发者来说,如果你使用kotlin一般都是java和kotlin混合使用的,所以首选的还是moshi这个库。如果是纯kotlin的话,还是选官方自带的比较好。
框架对比:
这里补充一个比较不错的库:klaxon 这个也是用来解析json的kotlin库,貌似支持的功能也比较丰富,后面有空再详细了解一下吧。这里先看一下moshi。
moshi的话,使用可以看官网:https://github.com/square/moshi 这里有一篇更详细的中文使用介绍:新一代Json解析库Moshi使用及原理解析
这里主要简单记录一下moshi的使用方式,首先gradle需要添加依赖:
还需要应用kotlin的apt插件,否则kapt()方法找不到
moshi官方提供了两种方式,一是反射方式,二是通过注解处理器,可以选其一,也可以都用。我上面的依赖是采用的注解处理器的方式,因为采用反射方式的话,需要引入一个额外的依赖库:
但是使用反射库会依赖导入一个2.5M大小的jar包,这么大。。这还得了!所以我们肯定不会选这种方式了。。。
然后就是代码,只需要在data class上面添加注解:
序列化和反序列化:
还有很多高级的用法,具体参考官网介绍。
上面的data类给定了默认值,moshi在序列化和反序列化时,是识别这个值的,如果没有给定值就采用默认值。
moshi更好的支持空类型安全,如果把上面的数据类的默认参数去掉:
moshi进行json反序列化为KClass时,如果filed是Nullable类型,则可以填入Null,如果是NonNull类型,则在填入Null时会立即抛出异常,将NPE风险暴露在早期,如果是java的话,则会到调用 person.name 的时候才会暴露。
也就是说,如果后台接口少返回了我们定义的data类中的非空类型的属性字段,在生成对象的时候就会报异常,而不是具体使用的时候。
moshi还支持属性延时初始化:
最后记录一个AS插件 JsonToKotlinClass ,类似于原来的GsonFormat插件,可以类似的根据Json字符串生成Kotlin的 data class
。
泛型
泛型约束:
多个约束:
多个泛型参数:
多个泛型参数,可以每个泛型都有约束:
类似的还有Map类,它的K 和V泛型都进行了约束:
协变
协变点:
协变点举例:
协变小结:
- 子类 Derived 兼容父类 Base
- 生产者
Producer<Derived>
兼容Producer<Base>
- 存在协变点的类的泛型参数必须声明为协变或不变
- 当泛型类作为泛型参数类实例的生产者时用协变
简而言之就是用out
关键字修饰的泛型就是协变, 返回值为协变泛型类型的称为协变点。
例子:
逆变
逆变点:
逆变小结:
- 子类 Derived 兼容父类 Base
- 生产者
Producer<Base>
兼容Producer<Derived>
- 存在逆变点的类的泛型参数必须声明为逆变或不变
- 当泛型类作为泛型参数类实例的消费者时用逆变
简而言之就是用in
关键字修饰的泛型就是逆变,作为函数输入参数的泛型称为逆变点。逆变点主要是指输入的参数类型,是消费者。并且消费者的继承关系跟协变是相反的。
例子:
星投影
-
*
可用在变量类型声明的位置 -
*
可用以描述一个未知的类型 -
*
所替换的类型在协变点返回泛型参数上限类型,在逆变点接收泛型参数下限类型
协变点:
星投影在所有逆变点的下限类型是Nothing, 因此不能用在属性或函数上。星投影的适用范围:
说白了只是一个描述符,可以简写泛型参数而已。
泛型擦除(伪泛型)
泛型实现原理:
泛型实现对比:
Java与Kotlin实现机制一样,在运行时擦除真正的类型,C#则会真的生成一个类型去执行。
内联特化
内联特化在调用的地方会替换到调用处,因此这时类型是确定的了,即已经特化成某个具体类型。通过fun前面的关键字 inline 和泛型参数T前面的 reified 参数两个来指定泛型参数在调用处实例化。
内联特化实际应用:
实例:模仿的Self Type
如果不定义SelfType类型,则子类在调用ConfirmNotificationBuilder().title(“Hello”)之后不能再继续调用子类的onCancel 方法,因为返回的是父类型,但是实际运行时这个类型是子类型。
实例: 基于泛型实现 Model 实例的注入
反射
反射的依赖:
反射常用数据结构:
反射常用数据结构:Kotlin vs Java
基本上和java是一一对应的,其中KType表示的是未擦除的泛型,KClass获取的是实际运行时的类型,不带泛型参数的。
Kotlin使用反射的话,唯一一点不好的就是需要引入一个体积非常大的库:
没错,有2.5M,不过编译后的大小还能接受
kotlin获取KClass通过两个冒号
KClass是不带泛型的类,typeOf能拿到具体的泛型实际类型:
拿到KClass之后可以通过KClass的方法获取各种其他属性了
一个简单的示例:
上面的class A继承了一个类,并且类的内部有其他类定义的扩展方法,本身也定义了一个扩展方法。
KClass提供了很多方法获取类的属性和方法,但是有一些区别,方法比较多,可以看一下区别:
可以看出以declared开头的方法基本上只能获取当前类的属性和方法,不带declared开头的方法则同时可以获取到父类的相关属性和方法。
还有一点需要注意的是,这里kotlin里面所指的扩展属性和扩展方法一般是指直接写在当前类中的其他类的扩展方法,如上面的A里面的String.hello()方法。如果是A类在某个地方定义的扩展方法是获取不到的,如上面的A.test()方法。这点跟java有点不一样。
nestedClasses
获取内部类
objectInstance
获取object单例的实例,如果不是单例类则返回可能为null
类内部的其他类如何获取外部类的实例对象:
java也是一样,内部类获取外部类的实例时需要通过,A.this
获取
获取泛型实参
1.获取接口某个方法的返回值类型的泛型参数
获取上面 Api 接口的 getUsers() 返回类型的泛型参数类 UserDTO
有几种方式,第一种是根据name来比较判断找到对应的方法:
还可以直接通过函数引用获取 Api::getUsers
得到的就是一个KFunction
显然这种方式最简单了。
还可以通过java的反射方式来获取:
只能说java的方式也可以,但是这种也太麻烦了。。还是全部用kotlin的方法吧,不然得各种强转各种判空?.
2.获取接口类的泛型
获取上面 SubType类实现的SuperType接口类的泛型:
关键代码就是这句:this::class.supertypes.first().arguments.first().type
这里的话主要注意这个this运行时是实际的子类型(OO多态),所以最后是可以直接强转的。
上面代码是只有一个父类,如果有多个父类,会有问题,需要修改一下:
获取上面 SubType2类的父类实现的SuperType接口类的泛型:
实例:为数据类实现 DeepCopy
调用测试代码:
上面的例子中主要有几点需要注意的:
-
this::class.isData
判断是否是数据类 data class -
this::class.primaryConstructor
获取主构造器,因为是数据类一定有主构造器,所以可以强转!!
-
primaryConstructor.parameters
let里面调用当前primaryConstructor对象的parameters获取所有的构造器参数 -
this::class as KClass<T>
逆变转协变,否则this.class返回一个协变点out T, 而get()方法接受一个逆变点,会报错 -
memberProperties.first { it.name == parameter.name }
数据类的特点是构造器的参数名和成员的属性名相等 -
parameter.type.classifier as? KClass<*>
type参数需要调用classifier先转成KClass然后再判断是否是数据类 -
parameter to value?.deepCopy()
如果value是数据类继续调用value的deepCopy()方法深拷贝,这里是一个递归调用,K to V 是返回的一个 Pair对象 -
.toMap()
将Pair集合转Map集合 -
.let(primaryConstructor::callBy)
调用主构造器,callsBy是KCallable接口的方法,KFunction是KCallable的子类,因此所有的KFunction都可以调用callBy,callBy接受一个map参数正好就是let前面返回的结果。
这个例子有一个完整的开源库代码 KotlinDeepCopy 是由大神Bennyhuo所写的,但是貌似目前没什么issue, 慎用,可以当案例学习一下。
实例:实现 Model 映射
这个例子是实现一个拷贝工作,将一个对象里的字段赋值给另一个对象里面的同名字段,跟深拷贝的例子有点相似
第一个方法的实现实际上是调用第二个方法的,所以只需实现第二个方法即可,这里依然是先获取主构造器To::class.primaryConstructor
,获取了主构造器之后拿到它的参数列表进行map操作,map里面依然是返回当前参数 parameter to value
,to 操作符左边的是To对象的也就是目标对象, to 操作符右边的是当前调用.mapAs的map对象,因此通过this[parameter.name]访问它里面的同名参数的value值,但是这个值可能为null, 不为null就返回 ?: 左边它自身,为null还需一个处理就是如果To类型即目标类的构造函数的这个当前参数可接受可空类型 ,就直接传null, 否则抛异常。
调用测试代码:
实例:可释放对象引用的不可空类型
这个例子主要是模仿了一个Android当中释放bitmap对象赋值为null的场景,在kotlin当中如果你定义了一个 var bitmap: Bitmap, 然后在onDestroy方法里面将其置为null, 但是这样写bitmap=null是不行的,因为定义的时候是一个不可空类型,这就矛盾了。
这个例子中主要利用了属性代理,然后有两个比较特殊的定义分别实现了属性代理接口KProperty0
的扩展属性KProperty0<*>.isInitialized
和扩展方法KProperty0<*>.release()
。
KProperty0
表示没有receiver(其实是绑定当前调用对象this作为receiver了)KProperty1
表示有1个receiver, KProperty2
表示有2个receiver。
isAccessible = true
允许反射操作,跟java一样也要设置一个accessible为true。this.getDelegate()
获取的是当前属性代理接口的实际代理对象,而 this.getDelegate() as? ReleasableNotNull<*>
这个是转换成实际类型,然后调用实际类型的相关属性或方法即可,当然这个对象可能为null或者不是一个ReleasableNotNull类型的,这时需要抛异常。
注解
注解定义:
使用annotation
关键字修饰calss前面,比java的 @interface
更人性化,其中@Retention
有三种:
分别表示作用时机是在源码级、编译期、还是运行时,跟java基本类似。
@Target
指定限定标注对象,取值如下:
添加参数:
注解类的参数是有限的,必须是能在编译期确定的类型。
简单使用:
内置注解:
第一个标注注解的注解主要是指前面的@Retention
和@Target
之类的,是写在注解类上的注解。
标准库的通用注解:
Java虚拟机相关注解:
像 @Synchronized
@Throws
注解都是比较好用的,替代java的相应关键字,比较人性化了。其中 @file:JvmName("KotlinAnnotations")
和 @file:JvmMultifileClass
比较有意思,能让多个文件中的kotlin代码最终生成到一个类里面,假如还有一个文件如下:
那经过编译之后,这个文件会和上面的文件合并到一起,生成到一个kotlin类文件当中。
实例:仿 Retrofit 反射读取注解请求网络
这个例子还是有点复杂,不太好理解,有些方法没接触过不知道啥意思,这里加了很多打印方法,把结果打印输出一下,这样能知道具体是代表的啥,就好理解一点了。
实例:注解加持反射版 Model 映射
这个例子是在前面反射一节实现的model映射例子的基础上,通过添加注解方式处理那些字段名称不是相同风格的情况,比如两个对象中的avatar_url
和 avatarUrl
的相互映射。
这里如果注解上不写@Retention(AnnotationRetention.RUNTIME)
默认就是运行时类型。
下面两种写法是等价的:
下面两种写法是等价的:
mapAs()方法中做了几件事:
- 尝试直接从当前Map中获取To对象的同名参数值,
- 尝试从To对象的字段上面的注解来获取需要转换的参数名,再根据名字获取Map中的值
- 尝试获取To对象的类注解得到处理类,调用处理类方法驼峰转下划线,再根据名字获取Map中的值
- 以上大招都没有获取到,如果To对象的字段可接受空值,就赋值null, 否则就抛异常
驼峰和下划线转换那里稍微有点绕。。
实例:注解处理器版 Model 映射
Java编译过程:
这个例子会用到一些著名的代码生成库:
- 生成Java代码:JavaPoet
- 生成Kotlin代码:KotlinPoet
上面两个都是square公司出品的开源库,JakeWharton大神的杰作,这个例子中主要用到了KotlinPoet 。
注解声明:
这里不需要在运行时保留注解,编译就会生成代码了,因此使用的是AnnotationRetention.BINARY
注解生成代码:
这是注解处理器的模块,然后新建一个模块来使用它:
gradle中必须通过kapt
的方式来依赖注解处理器的库:
注意注解处理器仅在编译器起作用,而注解库的依赖方式是在运行时,也就是会参与编译到字节码中的。
最后使用代码:
使用前先build一下,这样注解处理器会在下面的目录下生成kotlin的代码文件:
可以看到每个添加了@ModelMap
的data类都会生成一个文件 ,打开看一下内容:
基本就是前面期望的结果了
后面有空再把JavaPoet的使用拿出来学习一下吧
kotlin编译器插件对开发者的要求比较高,需要熟悉字节码的api。
AllOpen插件的处理逻辑:
常见的kotlin提供的插件:
kotlin插件还能编译js文件
kotlin 与 java 代码互调的一些注意事项
新建一个Kotlin文件,直接在里面写一个方法,编译器会生成一个 类名 + Kt 结尾的 java
类文件,并将该方法生成为该java
类的静态方法:
上面代码会生成类似下面Java代码:
直接在Java中调用的话,找到以 Kt 结尾的Java类进行静态方法调用即可:
但是如果Kotlin文件中的方法是写在一个类里面的,java调用的时候必须创建一个类实例进行调用:
另外需要注意: in
在 Kotlin 中是 关键字,Java 中如果有名字为 in
的变量名,在 Kotlin 中使用时需要写成 `in` 的方式来调用。
Kotlin 和 Java 互调的另一个坑:Kotlin 无法判断来自 Java 平台的可空性,所以最靠谱的方法是使用一个可空类型来接收来自 Java 的变量。例如,Java 中返回值为 String
类型的方法,在 Kotlin 中使用时,最好使用 var str : String ?
可空类型来接受,避免空指针风险。
Kotlin 中的只读集合并不是真的不可变,当跨平台的时候,它就可以被其他平台的语言所修改,例如,当我们在Kotlin中调用下面这个bar
方法的时候:
而这里调用的这个foo
方法是用Java的代码定义的:
所以传入bar
方法中的list
就会被foo
方法改变:
所以,当我们与Java进行互操作的时候就要考虑到这种情况。
关于Class
类名的传递:
- 如果方法参数的泛型是Java的类,必须传
Java类名::class.java
- 如果方法参数的泛型是Kotlin的类,直接传
Kotlin类型::class
即可
例如:
Kotlin中使用Java的Callback
写法:
建议使用第一种lambda的写法方式。
而 Kotlin 中使用 Kotlin 的 Callback
写法,只能使用object
的方式传,不能像调用 java 的callback
那样灵活:
示例:Handler
的Callback
和Thread
都是Java的类,可以多种写法
一个方法接受到可变参数传给另一个方法时,需要前面加上*
号:
另外需要注意的一点是,Java 并不支持主动指定一个函数是否是内联函数,所以在 Kotlin 中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数;而用reified
来实例化的参数类型的内联函数则不能在 Java 中调用,因为它永远是需要内联的。
如何将Kotlin方法与Java方法隔离
通常Java代码中可以直接调用kotlin的方法,但是可以通过以下方式使kotlin方法不被Java方法识别,只能被kotlin调用:
其中 4323545422666
这种写法可以向第三方调用者隐藏函数名意图,然后自己找个小本本记录下来 每个数字的含义,只有自己人知道具体含义。