Kotlin在设计之初,就考虑了与Java的互操作性。因此Java和Kotlin是可以很方便的进行互相调用的。虽然Kotlin完全兼容Java,但不代表Kotlin就是Java,它们在相互调用但时候,还是有一些需要注意的细节。
一、Kotlin 调 Java
首先,几乎所有的Java代码,都可以在Kotlin中调用而没有任何问题。如在Kotlin中使用集合类:
importjava.util.*fundemo(source:List){vallist = ArrayList()// “for”-循环用于 Java 集合:for(iteminsource) { list.add(item) }// 操作符约定同样有效:for(iin0..source.size -1) { list[i] = source[i]// 调用 get 和 set}}复制代码
只是在创建对象和使用对象方法的时候,可以有更简洁的方式去使用。
下面针对一些细节做详细介绍:
1、访问属性
如果要访问一个Java对象的私有属性,Java对象都会提供Getter 和 Setter方法,通过相关的Getter 和 Setter方法,就可以拿到属性的值。
而如果一个Java类为成员属性提供了Getter 和 Setter方法,则在Kotlin中使用该属性的时候,就可以直接通过属性名去访问,而不用调对应的Getter 和 Setter方法,如:
lateinitvartvHello: TextViewoverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvHello = findViewById(R.id.tvHello)// 为TextView设置显示内容tvHello.text ="hello,world!"// 获取TextView的显示内容Log.i(TAG,"onCreate:${tvHello.text}")}复制代码
请注意,如果 Java 类只有一个 setter方法,没有提供getter方法,它在 Kotlin 中不会作为属性可见,因为 Kotlin 目前不支持只写(set-only)属性。
这个时候,为属性赋值,就只能通过它的setter方法进行。
2、将 Kotlin 中是关键字的 Java 标识符进行转义
一些 Kotlin 关键字在 Java 中是有效标识符:in、object、is等等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,属性,你仍然可以通过反引号(`)字符转义它来调用该方法:
publicclassUser{publicObjectobject;publicvoidis(){ }}funtest(){valuser = User() user.`is`()// 调用is方法,需要加上反引号user.`object` = Object()// 访问属性名,需要加上反引号}复制代码
3、空安全与平台类型
平台类型:在Java中,所有的引用都可能为null,然而在Kotlin中,对null是有着严格的检查与限制的,这就使得某个来自于Java的引用在Kotlin中变得不再适合;
基于这个原因,在Kotlin中,将来自于Java的声明类型称为平台类型。
对于这种类型(平台类型)来说,Kotlin的null检查就得到一定的缓和,变得不再那么严格了。这样就使得空安全的语义要求变得与Java一致。
当我们调用平台类型引用的方法时,Kotlin就不会在编译期间施加空安全的检查,使得编译可以正常通过;但是在运行期间则有可能抛出异常,因为平台类型引用值有可能为null。
如:
Java类:
publicclassUser{publicString name;// name属性在没有赋值的时候,是可能为空的}复制代码
Kotlin类:
在使用Java的User类的时候,User类中的属性会被Kotlin当作是:平台类型,意思是,哪怕name属性是空的,也可以直接调用属性的相关方法,从而有可能导致空指针的发生。
funtest(){valuser = User()if(user.name.equals("李四")) { Log.i(TAG,"test: 坏人")return}}复制代码
如上面的代码,User对象创建后,没有给name属性赋值,然后直接就调用了name的比较方法,编译是可以通过的,但运行的时候就会报空指针异常。
解决方法:
为了避免调用Java代码可能产生的空指针,我们可以在使用平台类型变量的时候,通过“?.”的方式访问平台类型相关的属性和方法,从而触发Kotlin断言机制,达到预防空指针的目的,如:
funtest(){valuser = User()// 通过 ?. 的方式去方法平台类型的属性和方法,Kotlin会检测是否为空,如果为空,就不调用对象方法,从而避免空指针if(user.name?.length ==2) { println("test: 坏人") }// 编译期允许,运行时可能失败,还是可能会发生空指针,与直接调用没有本质区别// 如果name是null,则运行时,这里的赋值就会报空指针问题valuserName2:String = user.name }复制代码
如果我们使用了不可空类型,编译器会在赋值时生成一个断言,这会防止Kotlin的不可空变量持有null值;同样,这一点也适用于Kotlin方法参数传递,我们在将一个平台类型值传递给方法的一个不可空参数时,也会生成一个断言。
总体来说,Kotlin会竭尽所能防止null的赋值蔓延到程序的其他地方,而是在发生问题之处就立刻通过断言来解决。
注意:使用问号的声明方式,即:
valuserName: String? = user.name复制代码
4、已映射类型
Kotlin 特殊处理一部分 Java 类型。这样的类型不是“按原样”从 Java 加载,而是映射到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。
Java 的基础数据类型映射到相应的 Kotlin 类型
Java 类型Kotlin 类型
bytekotlin.Byte
shortkotlin.Short
intkotlin.Int
longkotlin.Long
charkotlin.Char
floatkotlin.Float
doublekotlin.Double
booleankotlin.Boolean
一些非原生的内置类型也会作映射:
Java 类型Kotlin 类型
java.lang.Objectkotlin.Any!
java.lang.Cloneablekotlin.Cloneable!
java.lang.Comparablekotlin.Comparable!
java.lang.Enumkotlin.Enum!
java.lang.Annotationkotlin.Annotation!
java.lang.CharSequencekotlin.CharSequence!
java.lang.Stringkotlin.String!
java.lang.Numberkotlin.Number!
java.lang.Throwablekotlin.Throwable!
Java 的装箱原始类型映射到可空的 Kotlin 类型:
Java typeKotlin type
java.lang.Bytekotlin.Byte?
java.lang.Shortkotlin.Short?
java.lang.Integerkotlin.Int?
java.lang.Longkotlin.Long?
java.lang.Characterkotlin.Char?
java.lang.Floatkotlin.Float?
java.lang.Doublekotlin.Double?
java.lang.Booleankotlin.Boolean?
Java 的数组按下文所述映射:
Java 类型Kotlin 类型
int[]kotlin.IntArray!
String[]kotlin.Array<(out) String>!
5、Java数组
Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特殊的类(IntArray、DoubleArray、CharArray等等)来处理这种情况。 它们与Array类无关,并且会编译成 Java 原生类型数组以获得最佳性能。
假设有一个接受 int 数组索引的 Java 方法:
publicclassJavaArrayExample{publicvoid removeIndices(int[] indices) {// // 在此编码……}}复制代码
在 Kotlin 中你可以这样传递一个原生类型的数组:
valjavaObj = JavaArrayExample()valarray = intArrayOf(0,1,2,3)// 构建一个int数组javaObj.removeIndices(array)// 将 int[] 传给方法复制代码
或者
valjavaObj = JavaArrayExample()valarray = IntArray(10)// 构建一个大小为10的int数组javaObj.removeIndices(array)// 将 int[] 传给方法复制代码
这样声明的数组,还是代表的基础数据类型的数组,不会存在基本数据类型的装箱与拆箱操作,性能时非常高的。Kotlin提供了原生类型数组如下:
Java类型Kotlin 类型
int[]IntArray!
long[]LongArray!
float[]FloatArray!
double[]DoubleArray!
char[]CharArray!
short[]ShortArray!
byte[]ByteArray!
boolean[]BooleanArray!
String[]Array<(out) String>!
6、Java 可变参数
Kotlin在调用Java中有可变参数的方法时,如果需要传递数组参数时,则需要使用展开运算符* 来传递数组参数:
publicclassUser{// 可变参数publicvoid setChildren(String... childrenName) {for(int i =0; i < childrenName.length; i++) { System.out.println("child name="+ childrenName[i]); } }}复制代码
funtest2(){valuser = User() user.setChildren("tom")// 手动传一个参数user.setChildren("tom","mike")// 传两个参数valnameArray = arrayOf("张三","李四","王五")// user.setChildren(nameArray) // 报错,无法通过编译user.setChildren(*nameArray)// 传数组参数user.setChildren(null)// 传null也可以,}复制代码
传null也可以,在查看转换的Java代码时候可以看到,传null的时候,是创建了一个String数组,包含了一个null的元素而已,如:
@Testpublicfinalvoidtest2(){ User user =newUser(); user.setChildren(newString[]{(String)null});}复制代码
7、受检异常
在Kotlin中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:
publicclassUser{publicvoid setChildren(String... childrenName) throws Exception {for(int i =0; i < childrenName.length; i++) { System.out.println("child name="+ childrenName[i]); } }}funtest2(){valuser = User() user.setChildren("tom")// 编译可以通过}复制代码
如果是Java调用setChildren方法的时候,需要用try catch捕获异常,或者向上抛出异常,否则无法通过编译,但Kotlin不会强制你捕获异常。
具体可以参考:浅谈Kotlin的Checked Exception机制
8、对象方法
当Java类型导入到Kotlin中时,类型java.lang.Object的所有引用都成了Any。 而因为Any不是平台指定的,它只声明了toString()、hashCode()和equals()作为其成员, 所以为了能用到java.lang.Object的其他成员,Kotlin 要用到扩展函数。
8-1、wait()/notify()
类型Any的引用没有提供wait()与notify()方法。通常不鼓励使用它们,而建议使用java.util.concurrent。 如果确实需要调用这两个方法的话,那么可以将引用转换为java.lang.Object:
(userasjava.lang.Object).wait()复制代码
8-2、getClass(),获取类的Class对象
要取得对象的 Java 类,请在类引用上使用java扩展属性:
valintent1 = Intent(this, MainActivity::class.java)复制代码
也可以使用扩展属性:javaClass,如:
valintent2 = Intent(this, MainActivity.javaClass)复制代码
8-3、clone()
Any 基类是没有声明**clone()**方法的,如果想覆盖clone(),需要继承kotlin.Cloneable:
classExample:Cloneable {overridefunclone(): Any { …… }}复制代码
8-4、finalize()
要覆盖finalize(),所有你需要做的就是简单地声明它,而不需要override关键字:
classC{protectedfunfinalize(){// 终止化逻辑}}复制代码
根据 Java 的规则,finalize()不能是private的。
9、SAM 转换
9-1、SAM转换详解
这里首先介绍两个概念:
函数式接口:只有一个抽象方法的接口叫函数式接口,也叫做:单一抽象方法接口。
SAM:即 Single Abstract Method Conversions,字面意思为:单一抽象方法转换,即把单一抽象方法接口转成lambda表达式 的过程叫做 单一抽象方法转换。
即函数式接口可以用lambda表达式代替。
如在Android中,如果要为一个 View 设置一个点击监听事件,我们会这样做:
view.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v){ System.out.println("click"); }});复制代码
这其实就是给View的setOnClickListener方法传一个OnClickListener类型的匿名内部类的对象。
在Kotlin中,也可以通过匿名内部类实现类似的功能:
tvHello.setOnClickListener(object: View.OnClickListener {overridefunonClick(v:View?){ println("click"); }})复制代码
但通过查看OnClickListener的源码可以看出,OnClickListener接口是一个函数式接口,既然是一个函数式的接口,就可以使用带接口类型前缀的lambda表达式替代手动创建实现函数式接口的类。如:
view.setOnClickListener(View.OnClickListener { System.out.println("click");})复制代码
而通过 SAM 转换, Kotlin 可以将其签名与接口的单个抽象方法的签名匹配的任何 lambda 表达式转换为实现该接口的类的实例,所以上面代码通过SAM可以进一步简化为:
view.setOnClickListener({ System.out.println("click");})复制代码
又因为Kotlin高阶函数的特性,如果lambda表达式是一个方法的最后一个参数,则可以把lambda表达式移到方法的小括号外面,即:
view.setOnClickListener() { System.out.println("click");}复制代码
如果方法只有一个参数且是lambda表达式,则方法调用的小括号也可以省略,所以最终的调用方式可以是:
view.setOnClickListener { System.out.println("click");}复制代码
9-2、 SAM 转换的歧义消除
假设有这样一个Java类,声明了两个重载方法,参数都是一个函数式接口,如:
publicclassSamInterfaceTest{// 函数式接口1publicinterfaceSamInterface1{voiddoWork(intvalue); }// 函数式接口2publicinterfaceSamInterface2{voiddoWork(intvalue); }privateSamInterface1 samInterface1;//privateSamInterface2 samInterface2;publicvoidsetSamInterface(SamInterface1 samInterface1){this.samInterface1 = samInterface1; }publicvoidsetSamInterface(SamInterface2 samInterface2){this.samInterface2 = samInterface2; }}复制代码
在Kotlin中通过SAM的方式去调用这个方法setSamInterface的时候,就会报错:
原因就是 SamInterface1 和 SamInterface2 的唯一抽象方法的函数类型都是:(Int)->Unit,而把函数式接口进行SAM转换的话,lambda表达式的函数类型也是:(Int)->Unit,这就导致Kotlin编译器无法确定到底该调用哪个方法,即SAM转换产生了歧义。
虽然这种情况比较奇葩,但也不排除会遇到,这个时候就需要我们消除歧义,消除歧义的方法有如下三种:
带接口类型前缀的lambda表达式
把lambda表达式进行强转
实现接口的匿名类
代码实现如下:
funtestSam(){valsam = SamInterfaceTest()// 方式1,带接口类型前缀的lambda表达式sam.setSamInterface(SamInterfaceTest.SamInterface1 { println("do something 1") })// 方式2,把lambda表达式进行强转sam.setSamInterface({ println("do something 2") }asSamInterfaceTest.SamInterface2)// 方式3,实现接口的匿名类sam.setSamInterface(object: SamInterfaceTest.SamInterface1 {overridefundoWork(value:Int){ println("do something 3") } })}复制代码
通过上面三种方式,就可以明确知道要调用哪个方法,从而消除歧义。
推荐使用:方式1,代码比较优雅,优雅很重要。
9-3、Kotlin函数式接口
在Kotlin 1.4之前,针对Java的函数式接口,Kotlin可以直接使用SAM转换,但对于 Kotlin 的函数式接口,却不能通过SAM转换,只能通过匿名内部类的方式实现接口参数的传递。
官方的解释是 Kotlin 本身已经有了函数类型和高阶函数等支持,所以不需要了再去转换了。如果你想使用类似的需要用 lambda 做参数的操作,应该自己去定义需要指定函数类型的高阶函数。
如:Kotlin1.4之前:
而在Kotlin 1.4(包含1.4)之后,Kotlin就开始支持函数式接口的SAM转换了,但对声明的接口有一定的限制,即接口必须使用fun关键字进行声明,如:
// 使用fun关键字,且接口只有一个抽象方法,这样的接口就是可以进行SAM转换的函数式接口funinterfaceSamInterface {funtest(value:Int)fun}复制代码
针对Kotlin函数式接口的转换:
classSamInterfaceTestKt{funtestSam(obj:SamInterface){ print("$obj") }}// 测试funtestKtSam(){valsamKt = SamInterfaceTestKt() samKt.testSam(SamInterface {// 带接口类型前缀的lambda表达式}) samKt.testSam {// lambda表达式}}复制代码
所以在Kotlin1.4之后,不管是Java的函数式接口,还是Kotlin的函数式接口,都可以进行SAM转换了。
9-4、SAM转换限制
SAM 转换的限制主要有两点 :
只支持Java接口
在Kotlin1.4之后,该限制就不存在了
只支持接口,不支持抽象类
这个官方没有多做解释。我想大概是为了避免混乱吧,毕竟如果支持抽象类的话,需要做强转的地方就太多了。而且抽象类本身是允许有很多逻辑代码在内部的,直接简写成一个 Lambda 的话,如果出了问题去定位错误的难度也加大了很多。
10、在Kotlin中使用JNI
Kotlin使用external表示函数是native(C/C++)代码实现。
externalfunfoo(x:Int):Double复制代码
二、Java 调 Kotlin
1、属性
一个Kotlin属性会编译为3部分Java元素
一个getter方法,名称通过加前缀get算出
一个setter方法,名称通过加前缀set算出(只适用于var属性);
一个私有的字段(field),其名字与Kotlin的属性名一样
如果Kotlin属性名以is开头,那么命名约定会发生一些变化:
getter方法与属性名一样
setter方法则是将is替换为set
一个私有的字段,其名字与Kotlin的属性名一样
举例说明:
Kotlin类:
classTestField{valage:Int=18varuserName: String ="Tom"varisStudent: String ="yes"}复制代码
编译生成的字节码对应的Java文件:
publicfinalclassTestField{// final 类型的属性,只有getter方法,没有setter方法privatefinalintage =18;@NotNullprivateString userName ="Tom";@NotNullprivateString isStudent ="yes";publicfinalintgetAge(){returnthis.age; }@NotNullpublicfinalStringgetUserName(){returnthis.userName; }publicfinalvoidsetUserName(@NotNullString var1){ Intrinsics.checkNotNullParameter(var1,"<set-?>");this.userName = var1; }@NotNullpublicfinalStringisStudent(){returnthis.isStudent; }publicfinalvoidsetStudent(@NotNullString var1){ Intrinsics.checkNotNullParameter(var1,"<set-?>");this.isStudent = var1; }}复制代码
从上面的代码可以看出:
通过val声明的属性,是final类型的,final 类型的属性,只有getter方法,没有setter方法
以is开头的属性,getter方法与属性名一样,setter方法把is替换为set。注意:这种规则适用于任何类型,而不单单是Boolean类型。
Kotlin属性,都会对应一个Java中的私有字段。
2、包级函数
2-1、基础介绍
我们知道,在Kotlin中,可以在Kotlin文件中声明一个类,声明属性,声明方法,这些都是允许的。而Kotlin编译器在编译的时候,会生成一个Kotlin文件对应的Java类,类名是:Kotlin文件名+Kt
文件中声明的属性,会变成该类中的静态私有属性,并提供getter和setter方法
文件中声明的方法,会变成该类中的静态公有方法
文件中声明的类,会生成对应的Java类
举例:
Kotlin文件:KotlinFile.kt
packagecom.mei.ktx.test/**
* 在Kotlin文件中,声明一个类
*/classClassInFile/**
* 在Kotlin文件中,声明一个方法
*/funcheckPhone(num:String):Boolean{ println("号码:$num")returntrue}/**
* 在Kotlin文件中,声明一个变量
*/varappName: String ="KotlinTest"复制代码
Kotlin编译器生成的对应的Java类:KotlinFileKt.java
publicfinalclassKotlinFileKt{// Kotlin文件中声明的属性,变成了Java类中的私有静态属性@NotNullprivatestaticString appName ="KotlinTest";// Kotlin文件中声明的方法,变成了Java类中的共有静态方法 publicstaticfinalbooleancheckPhone(@NotNullString num){ Intrinsics.checkNotNullParameter(num,"num"); String var1 ="号码:"+ num;booleanvar2 =false; System.out.println(var1);returntrue; }@NotNullpublicstaticfinalStringgetAppName(){returnappName; }publicstaticfinalvoidsetAppName(@NotNullString var0){ Intrinsics.checkNotNullParameter(var0,"<set-?>"); appName = var0; }}复制代码
Kotlin文件中声明的类,会生成一个独立的Java类,名称不变。
publicfinalclassClassInFile{}复制代码
所以Java调用Kotlin文件中的方法和属性的时候,需要通过对应的Java类名去调用:
publicstaticvoidmain(String[] args){// 通过类名直接调用KotlinFileKt.checkPhone("123456"); System.out.println(KotlinFileKt.getAppName());}复制代码
这里需要注意的是:Kotlin编译器自动生成的以Kt结尾的类,如:KotlinFileKt,是无法通过new关键字来创建对象的,因为在生成的字节码中没有构造方法的声明。
2-2、修改生成的类名
Kotlin文件所生成的Java类名,除了编译器默认生成的之外,还可以由自己指定,通过注解:@JvmName
如,Kotlin文件:
@file:JvmName("AppUtils")// 指定类名,需要在包名声明之前指定packagecom.mei.ktx.test/**
* 在Kotlin文件中,声明一个类
*/classClassInFile/**
* 在Kotlin文件中,声明一个方法
*/funcheckPhone(num:String):Boolean{ println("号码:$num")returntrue}/**
* 在Kotlin文件中,声明一个变量
*/varappName: String ="KotlinTest"复制代码
生成的Java类为:AppUtils
publicfinalclassAppUtils{@NotNullprivatestaticString appName ="KotlinTest";publicstaticfinalbooleancheckPhone(@NotNullString num){ Intrinsics.checkNotNullParameter(num,"num"); String var1 ="号码:"+ num;booleanvar2 =false; System.out.println(var1);returntrue; }@NotNullpublicstaticfinalStringgetAppName(){returnappName; }publicstaticfinalvoidsetAppName(@NotNullString var0){ Intrinsics.checkNotNullParameter(var0,"<set-?>"); appName = var0; }}复制代码
注意:
使用注解:@file:JvmName("类名")
在文件包名声明之前,指定类名
2-3、类名冲突解决
通过上面介绍,我们知道可以为Kotlin文件指定类名,但如果多个相同包名下的Kotlin文件所指定的类名相同,这就会造成类重复定义,导致编译不过。这个时候就可以借助注解:@JvmMultifileClass,把多个相同的类,合并成一个。
KotlinFile1.kt
@file:JvmName("LoginUtils")@file:JvmMultifileClasspackagecom.mei.ktx.testfuncheckPwd(password:String):Boolean{ println("密码:$password")returntrue}复制代码
KotlinFile2.kt
@file:JvmName("LoginUtils")@file:JvmMultifileClasspackagecom.mei.ktx.testfuncheckName(phone:String):Boolean{ println("号码:$phone")returntrue}复制代码
这样就没有冲突了,通过LoginUtils类就可以直接调用声明的方法:
publicstaticvoidmain(String[] args){ LoginUtils.checkName("abc"); LoginUtils.checkPwd("123456");}复制代码
注意:
在指定相同的类名的Kotlin文件中,都要加入该注解:@file:JvmMultifileClass
通常不建议自己指定类名。
3、实例字段
使用@JvmField注解对Kotlin中的属性进行标注时,表示它是一个实例字段(instance field),Kotlin编译器在编译的时候,就不会为这个属性生成对应的setter和getter方法,但可以直接访问这个属性,相当于是这个属性被声明称:public了。
Kotlin类:
classPerson{varname: String ="张三"@JvmFieldvarage:Int=18}复制代码
Java使用:
publicstaticvoidmain(String[] args){ Person person =newPerson(); System.out.println("name="+ person.getName() +";age="+ person.age);}复制代码
因为age被注解:@JvmField修饰了,所以在Java类中,age字段就被当成时public类型的,可以自己访问,且没有生成对应的getter和setter方法。
从生成的Java类中也可以看出来:
publicfinalclassPerson{@NotNullprivateString name ="张三";@JvmFieldpublicintage =18;// 共有属性@NotNullpublicfinalStringgetName(){returnthis.name; }publicfinalvoidsetName(@NotNullString var1){ Intrinsics.checkNotNullParameter(var1,"<set-?>");this.name = var1; }}复制代码
使用限制:如果一个属性有幕后字段(backing field)、非私有、没有open/override或者const修饰符并且不是被委托的属性,那么你可以用@JvmField注解该属性
感觉没啥用。
4、静态字段
4-1、Kotlin静态字段声明
Kotlin静态字段声明:在具名对象或伴生对象中声明的 Kotlin 属性,就是静态字段。它会在该具名对象或包含伴生对象的类中具有静态幕后字段。
如:伴生对象
classPerson{companionobject{varaliasName ="人"// 声明的静态字段}}复制代码
对应的Java类:
publicfinalclassPerson{privatestaticString aliasName;@NotNullpublicstaticfinalPerson.Companion Companion =newPerson.Companion((DefaultConstructorMarker)null);publicstaticfinalclassCompanion{@NotNullpublicfinalStringgetAliasName(){returnPerson.aliasName; }publicfinalvoidsetAliasName(@NotNullString var1){ Intrinsics.checkNotNullParameter(var1,"<set-?>"); Person.aliasName = var1; } }}复制代码
从上面的Java代码也可以看出,这样声明的静态字段,是私有的静态字段,在Java中调用使用这样的静态字段,需要通过生成的伴生类:Companion对象去获取和赋值,因为自动有生成getter和setter方法。
publicstaticvoidmain(String[] args){ System.out.println("alias="+ Person.Companion.getAliasName());}复制代码
4-2、静态字段公有化
通过上面声明的静态字段,默认是私有的静态字段,但我们可以通过如下方法,将私有字段变为公有字段:
使用@JvmField注解 修饰字段
使用lateinit修饰符 修饰字段
使用const修饰符 修饰字段
如:
classPerson{companionobject{// 使用const修饰constvalTAG ="Person"// 使用lateinit修饰lateinitvaraliasName: String// 使用注解@JvmFieldvarage:Int=18}}复制代码
通过上面三种方式修饰的静态字段,都是公有的静态字段,这个时候访问的时候,就可以直接通过类名去访问,不需要借助伴生类:Companion去访问。如:
publicstaticvoidmain(String[] args){ System.out.println("alias="+ Person.TAG); Person.aliasName ="人"; System.out.println("alias="+ Person.aliasName); System.out.println("alias="+ Person.age); System.out.println("alias="+ Person.Companion.getAliasName()); }复制代码
通过lateinit修饰的静态字段,虽然是公有的静态字段,但在伴生对象中,还是会生成对应的setter和getter方法。
区别:
const和**@JvmField**修饰的静态字段,无法通过伴生对象访问,也不会生成对应的setter和getter方法。
lateinit修饰的静态字段,可以通过伴生对象访问,也可以直接通过类名访问,且伴生类中还会生成对应的setter和getter方法。
5、静态方法
如上所述,Kotlin 将包级函数表示 为静态方法。
Kotlin 在具名对象或伴生对象中定义的函数,默认情况下不是静态的,如果想声明一个静态的函数,则可以用@JvmStatic注解修饰方法,这样声明的方法就是静态方法。
调用方式:
可以直接通过类名调用
也可以通过伴生对象调用。
Kotlin中,在伴生对象中声明静态方法:
classPerson{companionobject{funnotStaticMethod(){ println("不是静态方法") }@JvmStaticfunstaticMethod(){ println("是静态方法") } }}复制代码
上面代码,在伴生对象中声明了两个方法,通过注解:@JvmStatic修饰的是静态方法,在Java中可以直接通过类名调用,也可以通过伴生对象调用。
@Testpublicvoidtest2(){ Person.Companion.staticMethod();// 通过伴生对象调静态方法Person.staticMethod();// 通过类名调用静态方法Person.Companion.notStaticMethod();// 通过版本对象,调用非静态方法}复制代码
@JvmStatic注解也可以应用于对象或伴生对象的属性,使得该属性在该类中也有静态的 getter 和 setter 方法。
6、签名冲突
通过注解:@JvmName,可以解决函数签名冲突的问题。
6-1、泛型檫除导致的签名冲突
最突出的例子是由于类型擦除引发的:
funList<String>.filterValid(): List {returnarrayListOf("hello","world")}funList<Int>.filterValid(): List {returnarrayListOf(1,2,3)}复制代码
在Kotlin文件中,定义上面两个扩展函数,是无法通过编译的,会提示报错:
即Kotlin在编译成字节码的时候,泛型会被檫除,导致两个方法在JVM看来,方法签名是一样的,都是:filterValid(Ljava/util/List;)Ljava/util/List;
这样JVM会认为这两个方法是同一个方法,但却被重复定义了。
解决办法是,通过注解@JvmName给方法重新指定一个名字,如:
funList<String>.filterValid(): List {returnarrayListOf("hello","world")}@JvmName("filterValidInt")// 重新指定方法名称funList<Int>.filterValid(): List {returnarrayListOf(1,2,3)}vallist=list(1,2,3)list.复制代码
这样就可以编译通过了。
在 Kotlin 中它们可以用相同的名称filterValid来访问,而在 Java 中,它们分别是filterValid和filterValidInt。
Java中调用:
@Testpublicvoidtest3(){ List stringList =newArrayList<>(); System.out.println(ListExternalKt.filterValid(stringList)); List integerList =newArrayList<>(); System.out.println(ListExternalKt.filterValidInt(integerList));}复制代码
Kotlin中调用:
funmain(){valstringList = arrayListOf() println(stringList.filterValid())valintList = arrayListOf() println(intList.filterValid())// Kotlin调用的时候,直接就可以用方法名,而不是用重定义的方法名}复制代码
输出:
6-2、属性的getter和setter方法与类中的现有方法冲突
同样的技巧也适用于属性x和函数getX()共存:
valx:Int@JvmName("getXValue")get() =15fungetX()=10复制代码
如需在没有显式实现 getter 与 setter 的情况下更改属性生成的访问器方法的名称,可以使用**@get:JvmName** 与@set:JvmName:
classPerson{@get:JvmName("getXValue")@set:JvmName("setXValue")varx:Int=20}复制代码
Java中调用:
@Testpublicvoidtest3(){ Person person =newPerson(); person.setXValue(20);}复制代码
7、生成重载
通常,如果你写一个有默认参数值的 Kotlin 函数,在Kotlin编译器生成的字节码中,只会有这么一个完整参数的方法,则Java调用这个方法的时候,需要传完整的参数,不可缺少。
如:Kotlin中定义了一个 Fruit 类,有一个两个参数的主构造函数,其中有一个参数有默认值
classFruitconstructor(varname: String,vartype:Int=1) {// 有默认参数的构造函数// 有默认参数的方法funsetFuture(color:String, size:Int=1){ }}复制代码
如果是在Kotlin中创建这个Fruit对象,则可以只传一个参数,默认参数可以不传。
但如果是在Java中创建这个Fruit对象,则两个参数都必须传,因为Java是不支持默认参数的。Fruit生成的字节码中,也只有这一个构造函数。如:
只传一个参数的话,Java编译不通过。
那可不可以让编译器帮我们生成多个重载的方法,当然是可以的。即可以使用@JvmOverloads注解来实现。
如:
给方法增加**@JvmOverloads**注解:
classFruit@JvmOverloadsconstructor(varname: String,vartype:Int=1) {@JvmOverloadsfunsetFuture(color:String, size:Int=1){ }}复制代码
这个时候在Java中创建Fruit对象,调用setFuture方法,都可以只传一个参数了:
本质原因是,Kotlin编译器在生成字节码的时候,为增加了@JvmOverloads注解的方法,增加了多个重载方法,如查看Fruit类的Java代码如下:
publicfinalclassFruit{@NotNullprivateString name;privateinttype;// 两个参数的setFuture方法@JvmOverloadspublicfinalvoidsetFuture(@NotNullString color,intsize){ Intrinsics.checkNotNullParameter(color,"color"); }// $FF: synthetic methodpublicstaticvoidsetFuture$default(Fruit var0, String var1,intvar2,intvar3, Object var4) {if((var3 &2) !=0) { var2 =1; } var0.setFuture(var1, var2); }// 一个参数的setFuture方法@JvmOverloadspublicfinalvoidsetFuture(@NotNullString color){ setFuture$default(this, color,0,2, (Object)null); }// 两个参数的构造函数@JvmOverloadspublicFruit(@NotNullString name,inttype){ Intrinsics.checkNotNullParameter(name,"name");super();this.name = name;this.type = type; }// $FF: synthetic methodpublicFruit(String var1,intvar2,intvar3, DefaultConstructorMarker var4){if((var3 &2) !=0) { var2 =1; }this(var1, var2); }// 一个参数的构造函数@JvmOverloadspublicFruit(@NotNullString name){this(name,0,2, (DefaultConstructorMarker)null); }}复制代码
正是因为Kotlin编译器帮我们生成了对用的重载方法,我们才可以调用。
8、受检异常
我们知道,Kotlin是没有受检异常的,所以Kotlin函数的Java签名不会声明抛出异常。 于是如果我们有一个这样的 Kotlin 函数:
FileUtils文件:
funwriteToFile(){ println("写入文件")throwIOException()// 在Kotlin方法中,抛出了一个IO异常}复制代码
然后我们想要在 Java 中调用它并捕捉这个异常:
如果我们尝试去捕获这个IO异常,Java就会报错。原因是writeToFile()未在 throws 列表中声明 IOException。所以在调用这个方法的时候,不能捕获到IOException。
为了解决Kotlin异常无法向上抛的问题,Kotlin提供了注解:@Throws来解决这个问题
使用如下:
给需要向上抛异常的方法,增加**@Throws注解,并在注解上指明异常的类型,这里的类型是KClass**类型。
@Throws(IOException::class)funwriteToFile(){ println("写入文件")throwIOException()// 在Kotlin方法中,抛出了一个IO异常}复制代码
通过注解**@Throws**,就可以把异常向上抛了,这样在Java调用Kotlin方法的时候,就可以捕获对应的异常了,如:
增加注解后,Java可以正常捕获到IOException异常了。
9、空安全
在Java调用Kotlin函数时,无法防止将null作为非空参数传递给函数。所以Kotlin为所有期望非空参数的public函数生成运行时检查。这样会在Java代码中立即出现NullPointerException异常。
funcheckPhone(num:String):Boolean{ println("号码:$num")returntrue}复制代码
Kotlin中的checkPhone方法,参数是非空类型的,在Java中调用这个方法时,如果传一个null的话,在运行时就会报空指针异常,如:
可以看到运行的时候,就报异常了。同时,在编译器也给我们提醒了,当传null的时候,报黄了。
如果Kotlin方法定义的时候,参数声明为可空类型,那么在Java中调用的时,传一个null,运行时就不会报空指针异常了:
// 参数声明为可空类型funcheckPhone(num:String?):Boolean{ println("号码:$num")returntrue}复制代码
三、Android KTX使用
1、简述
Android KTX是包含在AndroidJetpack及其他Android库中的一组Kotlin扩展程序。KTX扩展程序可以为Jetpack、Android平台及其他API提供简洁的惯用Kotlin代码。为此,这些扩展程序利用了多种Kotlin语言功能,其中包括:
扩展函数
扩展属性
Lambda
命名参数
参数默认值
协程
通过KTX中的扩展API,可以帮助我们用更少的代码实现复杂的功能,就像是工具类一样,帮助我们减少了重复代码的编写,而只需要关注自己的核心代码实现。
例如:通常使用SharedPreferences时,您必须先创建一个编辑器,然后才能对偏好设置数据进行修改。在完成修改后,您还必须应用或提交这些更改,如以下示例所示:
sharedPreferences .edit()// create an Editor.putBoolean("key", value) .apply()// write to disk asynchronously复制代码
其实对于开发者来说,获取Editor对象,最后的提交操作,对于每一次存/取来说,都是重复的操作,冗余的代码,对开发者应该屏蔽才对。真正需要关心的是存/取操作。
那么这些代码可不可以省略不写呢?当然可以,在Java中,我们就会通过封装一个工具类,来执行这些存/取操作,一行代码就搞定。
而在Kotlin中,就可以使用Google提供的KTX库来实现,如:
sharedPreferences.edit { putBoolean("key", value) }复制代码
上面的edit方法,是Android KTX Core库中,为SharedPreferences增加的扩展函数,在调用这个扩展函数的时候,需要传一个lambda表达式,在这个lambda表达式中,就可以直接调用Editor类中的put*相关方法,进行数据的保存而不用关心其他的任何操作,这即节省了代码又提高了开发效率。
下面看一下Android KTX Core库中,为SharedPreferences增加的扩展函数edit的源码:
@SuppressLint("ApplySharedPref")inlinefunSharedPreferences.edit( commit:Boolean=false,// 是否通过commit方法提交数据,默认通过apply方法提交数据action:SharedPreferences.Editor.() ->Unit// 表达式){valeditor = edit()// 获取Editor对象action(editor)// 执行lambda表达式,即执行用户的代码if(commit) { editor.commit()// 提交数据}else{ editor.apply() }}复制代码
通过上面的源码可以看出,edit方法,帮我们实现了需要重复编写的代码,让开发者只关注于自己的功能实现,从而减少代码量并提升效率。
2、项目中使用Android KTX
上面的针对SharedPreferences的扩展函数,定义在Android KTX Core核心库中,而Google针对不同的功能库,都提供了不同的扩展库,以更好的服务各个功能库,如:
扩展库名称依赖描述
Core KTXimplementation"androidx.core:core-ktx:1.3.2"核心扩展库
Collection KTXimplementation"androidx.collection:collection-ktx:1.1.0"集合扩展库
Fragment KTXimplementation "androidx.fragment:fragment-ktx:1.3.1"Fragment扩展库
Lifecycle KTXimplementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0"声明周期扩展库
LiveData KTXimplementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"LiveData扩展库
ViewModel KTXimplementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"ViewModel扩展库
上面列举了一些常用的扩展库,还有其他扩展库没有列举出来,如果想要查看的话,可以去官网查看:Android KTX
下面介绍一些常用扩展库的常规用法:
2-1、Android KTX Core 核心库
上面的针对SharedPreferences的扩展函数,定义在Android KTX Core核心库中,如果需要在项目中使用,则需要在module工程的build.gradle文件中,添加依赖:
dependencies { implementation"androidx.core:core-ktx:1.3.2"}复制代码
引入该库之后,就可以使用相关的扩展API了。
(1)、动画相关
针对动画的监听增加了一些扩展函数,避免了实现接口和实现方法,使用如下:
funstartAnimation(){valanimation = ValueAnimator.ofFloat(0f,360f) .setDuration(500) animation.doOnCancel {// 监听取消回调} animation.doOnEnd {// 监听动画的结束} animation.start()}复制代码
个人感觉比较鸡肋,每个扩展函数都会为动画增加一个监听对象,比如上面调用了两个扩展函数,就给animation对象增加了两个监听对象,感觉不划算,还增加了回调的成本。
(2)、Context相关
方法列表:developer.android.com/kotlin/ktx/…
比较实用的:解析自定义属性
Context.withStyledAttributes(set: AttributeSet? =null, attrs: IntArray,@AttrResdefStyleAttr:Int=0,@StyleResdefStyleRes:Int=0, block: TypedArray.() ->Unit)复制代码
使用:
classCusTextView@JvmOverloadsconstructor( context: Context?, attrs: AttributeSet? =null, defStyleAttr:Int=0) : TextView(context, attrs, defStyleAttr) {init{// 通过这个方法,可以在lambda表达式中,直接解析自定义属性,挺实用的context?.withStyledAttributes(attrs, R.styleable.ActionBar) { cusBgColor = getColor(R.styleable.CusTextView_cusBgColor,0) } }}复制代码
通过Context的withStyledAttributes方法,可以在lambda表达式中,直接解析自定义属性,挺实用的。
(3)、Canvas 相关
扩展函数功能描述
Canvas.[withClip](developer.android.com/reference/k…, kotlin.Function1))(clipRect:Rect, block:Canvas.() ->Unit)按照指定的大小,裁剪画布,在执行block之前,
1. 先调用Canvas.save和Canvas.clip方法,
2. 接着调用block,
3. 最后执行Canvas.restoreToCount方法
相当于Kotlin编译器帮我们做了画布的保存裁剪与恢复操作,开发者只需要关心绘制就好
Canvas.withRotation旋转画布,然后执行block绘制,最后恢复画布状态。
Canvas.withScale缩放画布,然后执行block绘制,最后恢复画布状态。
Canvas.withTranslation平移画布,然后执行block绘制,最后恢复画布状态。
Canvas.withSkew斜拉画布,然后执行block绘制,最后恢复画布状态。
Canvas.withSave保存原图层,然后执行block绘制,最后恢复画布状态
Canvas.withMatrix画布执行举证变换,然后执行block绘制,最后恢复画布状态
Canvas这一系列的扩展函数,帮我们省去了画布的状态保存和恢复,并执行相应的操作,让开发者只关注于绘制本身,非常实用。
classCusTextView@JvmOverloadsconstructor( context: Context?, attrs: AttributeSet? =null, defStyleAttr:Int=0) : TextView(context, attrs, defStyleAttr) {overridefunonDraw(canvas:Canvas?){super.onDraw(canvas)// 把画布先裁剪成一个大小为100的正方形,然后给这个正方形绘制一个绿色的背景色,我们只关注绘制颜色本身,而不用去管画布// 的裁剪,画布状态的保存与恢复canvas?.withClip(Rect(0,0,100,100)) { drawColor(Color.GREEN) } }}复制代码
上面代码中,把画布先裁剪成一个大小为100的正方形,然后给这个正方形绘制一个绿色的背景色,我们只关注绘制颜色本身,而不用去管画布的裁剪,画布状态的保存与恢复,使用起来非常的简单。
在自定义View的时候,这些方法帮助很大。
(4)、SparseArray集合
KTX Core 为SparseArray相关的类,增加了很多的扩展函数,如:
遍历元素:SparseArray.forEach(action: (key:Int, value: T) ->Unit)
获取元素,有默认值:SparseArray.[getOrDefault](developer.android.com/reference/k…, androidx.core.util.android.util.SparseArray.getOrDefault.T))(key:Int, defaultValue: T)
集合判空:SparseLongArray.isEmpty()
如:
funtest(){valmap = SparseArray()if(map.isNotEmpty()) { map.forEach { key, value -> println("key=$key,value=$value") } }}复制代码
通过扩展函数,很方便的就可以便利SparseArray集合。
(5)、View和ViewGroup
View的扩展函数:
更新LayoutParams:View.updateLayoutParams(block:LayoutParams.() ->Unit),这样就不用每次修改都去获取LayoutParams,然后设值了
把View转换成Bitmap:View.drawToBitmap(config:Config= Bitmap.Config.ARGB_8888)
监听声明周期方法,如:
视图附加到窗口时:View.doOnAttach(crossinline action: (view:View) ->Unit)
视图与窗口分离:View.doOnDetach(crossinline action: (view:View) ->Unit)
View.doOnLayout(crossinline action: (view:View) ->Unit)
View.doOnNextLayout(crossinline action: (view: View) -> Unit)
View.doOnPreDraw(crossinline action: (view:View) ->Unit)
ViewGroup扩展函数:
是否包含指定的View:ViewGroup.contains(view:View)
遍历子View:ViewGroup.forEach(action: (view:View) ->Unit),并执行相关操作