Kotlin与Java相互调用详解以及Android KTX的使用

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 中是有效标识符:inobjectis等等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,属性,你仍然可以通过反引号(`)字符转义它来调用该方法:

publicclassUser{publicObjectobject;publicvoidis(){    }}funtest(){valuser = User()    user.`is`()// 调用is方法,需要加上反引号user.`object` = Object()// 访问属性名,需要加上反引号}复制代码

3、空安全与平台类型

平台类型:在Java中,所有的引用都可能为null,然而在Kotlin中,对null是有着严格的检查与限制的,这就使得某个来自于Java的引用在Kotlin中变得不再适合;

基于这个原因,在Kotlin中,将来自于Java的声明类型称为平台类型

对于这种类型(平台类型)来说,Kotlinnull检查就得到一定的缓和,变得不再那么严格了。这样就使得空安全的语义要求变得与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");    }});复制代码

这其实就是给ViewsetOnClickListener方法传一个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编译器在编译的时候,就不会为这个属性生成对应的settergetter方法,但可以直接访问这个属性,相当于是这个属性被声明称: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类型的,可以自己访问,且没有生成对应的gettersetter方法。

从生成的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对象去获取和赋值,因为自动有生成gettersetter方法。

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扩展程序可以为JetpackAndroid平台及其他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)        }    }}复制代码

通过ContextwithStyledAttributes方法,可以在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),并执行相关操作

是否不包含任何子View:ViewGroup.isEmpty()

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容