Kotlin在设计之初,就考虑了与Java的互操作性。因此Java和Kotlin是可以很方便的进行互相调用的。虽然Kotlin完全兼容Java,但不代表Kotlin就是Java,它们在相互调用但时候,还是有一些需要注意的细节。
一、Kotlin 调 Java
首先,几乎所有的Java代码,都可以在Kotlin中调用而没有任何问题。如在Kotlin中使用集合类:
import java.util.*
fun demo(source: List<Int>) {
val list = ArrayList<Int>()
// “for”-循环用于 Java 集合:
for (item in source) {
list.add(item)
}
// 操作符约定同样有效:
for (i in 0..source.size - 1) {
list[i] = source[i] // 调用 get 和 set
}
}
只是在创建对象和使用对象方法的时候,可以有更简洁的方式去使用。
下面针对一些细节做详细介绍:
1、访问属性
如果要访问一个Java对象的私有属性,Java对象都会提供Getter 和 Setter方法,通过相关的Getter 和 Setter方法,就可以拿到属性的值。
而如果一个Java类为成员属性提供了Getter 和 Setter方法,则在Kotlin中使用该属性的时候,就可以直接通过属性名去访问,而不用调对应的Getter 和 Setter方法,如:
lateinit var tvHello: TextView
override fun onCreate(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 关键字作为方法,属性,你仍然可以通过反引号(`)字符转义它来调用该方法:
public class User {
public Object object;
public void is(){
}
}
fun test(){
val user = User()
user.`is`() // 调用is方法,需要加上反引号
user.`object` = Object() // 访问属性名,需要加上反引号
}
3、空安全与平台类型
平台类型:在Java中,所有的引用都可能为null,然而在Kotlin中,对null是有着严格的检查与限制的,这就使得某个来自于Java的引用在Kotlin中变得不再适合;
基于这个原因,在Kotlin中,将来自于Java的声明类型称为平台类型。
对于这种类型(平台类型)来说,Kotlin的null检查就得到一定的缓和,变得不再那么严格了。这样就使得空安全的语义要求变得与Java一致。
当我们调用平台类型引用的方法时,Kotlin就不会在编译期间施加空安全的检查,使得编译可以正常通过;但是在运行期间则有可能抛出异常,因为平台类型引用值有可能为null。
如:
Java类:
public class User {
public String name;// name属性在没有赋值的时候,是可能为空的
}
Kotlin类:
在使用Java的User类的时候,User类中的属性会被Kotlin当作是:平台类型,意思是,哪怕name属性是空的,也可以直接调用属性的相关方法,从而有可能导致空指针的发生。
fun test() {
val user = User()
if (user.name.equals("李四")) {
Log.i(TAG, "test: 坏人")
return
}
}
如上面的代码,User对象创建后,没有给name属性赋值,然后直接就调用了name的比较方法,编译是可以通过的,但运行的时候就会报空指针异常。
解决方法:
为了避免调用Java代码可能产生的空指针,我们可以在使用平台类型变量的时候,通过“ ?. ”的方式访问平台类型相关的属性和方法,从而触发Kotlin断言机制,达到预防空指针的目的,如:
fun test() {
val user = User()
// 通过 ?. 的方式去方法平台类型的属性和方法,Kotlin会检测是否为空,如果为空,就不调用对象方法,从而避免空指针
if (user.name?.length == 2) {
println("test: 坏人")
}
// 编译期允许,运行时可能失败,还是可能会发生空指针,与直接调用没有本质区别
// 如果name是null,则运行时,这里的赋值就会报空指针问题
val userName2:String = user.name
}
如果我们使用了不可空类型,编译器会在赋值时生成一个断言,这会防止Kotlin的不可空变量持有null值;同样,这一点也适用于Kotlin方法参数传递,我们在将一个平台类型值传递给方法的一个不可空参数时,也会生成一个断言。
总体来说,Kotlin会竭尽所能防止null的赋值蔓延到程序的其他地方,而是在发生问题之处就立刻通过断言来解决。
注意:使用问号的声明方式,即:
val userName: String? = user.name
4、已映射类型
Kotlin 特殊处理一部分 Java 类型。这样的类型不是“按原样”从 Java 加载,而是 映射 到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。
-
Java 的基础数据类型映射到相应的 Kotlin 类型
Java 类型 Kotlin 类型 byte
kotlin.Byte
short
kotlin.Short
int
kotlin.Int
long
kotlin.Long
char
kotlin.Char
float
kotlin.Float
double
kotlin.Double
boolean
kotlin.Boolean
-
一些非原生的内置类型也会作映射:
Java 类型 Kotlin 类型 java.lang.Object
kotlin.Any!
java.lang.Cloneable
kotlin.Cloneable!
java.lang.Comparable
kotlin.Comparable!
java.lang.Enum
kotlin.Enum!
java.lang.Annotation
kotlin.Annotation!
java.lang.CharSequence
kotlin.CharSequence!
java.lang.String
kotlin.String!
java.lang.Number
kotlin.Number!
java.lang.Throwable
kotlin.Throwable!
-
Java 的装箱原始类型映射到可空的 Kotlin 类型:
Java type Kotlin type java.lang.Byte
kotlin.Byte?
java.lang.Short
kotlin.Short?
java.lang.Integer
kotlin.Int?
java.lang.Long
kotlin.Long?
java.lang.Character
kotlin.Char?
java.lang.Float
kotlin.Float?
java.lang.Double
kotlin.Double?
java.lang.Boolean
kotlin.Boolean?
-
Java 的数组按下文所述映射:
Java 类型 Kotlin 类型 int[]
kotlin.IntArray!
String[]
kotlin.Array<(out) String>!
5、Java数组
Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特殊的类(IntArray
、 DoubleArray
、 CharArray
等等)来处理这种情况。 它们与 Array
类无关,并且会编译成 Java 原生类型数组以获得最佳性能。
假设有一个接受 int 数组索引的 Java 方法:
public class JavaArrayExample {
public void removeIndices(int[] indices) {
//
// 在此编码……
}
}
在 Kotlin 中你可以这样传递一个原生类型的数组:
val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)// 构建一个int数组
javaObj.removeIndices(array) // 将 int[] 传给方法
或者
val javaObj = JavaArrayExample()
val array = 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中有可变参数的方法时,如果需要传递数组参数时,则需要使用展开运算符* 来传递数组参数:
public class User {
// 可变参数
public void setChildren(String... childrenName) {
for (int i = 0; i < childrenName.length; i++) {
System.out.println("child name=" + childrenName[i]);
}
}
}
fun test2() {
val user = User()
user.setChildren("tom") // 手动传一个参数
user.setChildren("tom", "mike")// 传两个参数
val nameArray = arrayOf("张三", "李四", "王五")
// user.setChildren(nameArray) // 报错,无法通过编译
user.setChildren(*nameArray)// 传数组参数
user.setChildren(null) // 传null也可以,
}
传null也可以,在查看转换的Java代码时候可以看到,传null的时候,是创建了一个String数组,包含了一个null的元素而已,如:
@Test
public final void test2() {
User user = new User();
user.setChildren(new String[]{
(String)null});
}
7、受检异常
在 Kotlin 中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:
public class User {
public void setChildren(String... childrenName) throws Exception {
for (int i = 0; i < childrenName.length; i++) {
System.out.println("child name=" + childrenName[i]);
}
}
}
fun test2() {
val user = 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
:
(user as java.lang.Object).wait()
8-2、getClass(),获取类的Class对象
要取得对象的 Java 类,请在类引用上使用 java
扩展属性:
val intent1 = Intent(this, MainActivity::class.java)
也可以使用扩展属性:javaClass,如:
val intent2 = Intent(this, MainActivity.javaClass)
8-3、clone()
Any 基类是没有声明**clone()**方法的,如果想覆盖 clone(),需要继承 kotlin.Cloneable:
class Example : Cloneable {
override fun clone(): Any {
…… }
}
8-4、finalize()
要覆盖 finalize()
,所有你需要做的就是简单地声明它,而不需要 override 关键字:
class C {
protected fun finalize() {
// 终止化逻辑
}
}
根据 Java 的规则,finalize()
不能是 private 的。
9、SAM 转换
9-1、SAM转换详解
这里首先介绍两个概念:
-
函数式接口:只有一个抽象方法的接口叫函数式接口,也叫做:单一抽象方法接口。
-
SAM:即 Single Abstract Method Conversions,字面意思为:单一抽象方法转换,即把 单一抽象方法接口 转成 lambda表达式 的过程叫做 单一抽象方法转换。
即 函数式接口 可以用 lambda 表达式代替。
如在Android中,如果要为一个 View 设置一个点击监听事件,我们会这样做:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("click");
}
});
这其实就是给View的setOnClickListener方法传一个OnClickListener类型的匿名内部类的对象。
在Kotlin中,也可以通过匿名内部类实现类似的功能:
tvHello.setOnClickListener(object : View.OnClickListener {
override fun onClick(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类,声明了两个重载方法,参数都是一个 函数式接口,如:
public class SamInterfaceTest {
// 函数式接口1
public interface SamInterface1 {
void doWork(int value);
}
// 函数式接口2
public interface SamInterface2 {
void doWork(int value);
}
private SamInterface1 samInterface1;//
private SamInterface2 samInterface2;
public void setSamInterface(SamInterface1 samInterface1) {
this.samInterface1 = samInterface1;
}
public void setSamInterface(SamInterface2 samInterface2) {
this.samInterface2 = samInterface2;
}
}
在Kotlin中通过SAM的方式去调用这个方法setSamInterface的时候,就会报错:
原因就是 SamInterface1 和 SamInterface2 的唯一抽象方法的函数类型都是:(Int)->Unit,而把函数式接口进行 SAM 转换的话,lambda表达式的函数类型也是:(Int)->Unit,这就导致Kotlin编译器无法确定到底该调用哪个方法,即SAM转换产生了歧义。
虽然这种情况比较奇葩,但也不排除会遇到,这个时候就需要我们消除歧义,消除歧义的方法有如下三种:
- 带接口类型前缀的lambda表达式
- 把lambda表达式进行强转
- 实现接口的匿名类
代码实现如下:
fun testSam() {
val sam = SamInterfaceTest()
// 方式1,带接口类型前缀的lambda表达式
sam.setSamInterface(SamInterfaceTest.SamInterface1 {
println("do something 1")
})
// 方式2,把lambda表达式进行强转
sam.setSamInterface({
println("do something 2")
} as SamInterfaceTest.SamInterface2)
// 方式3,实现接口的匿名类
sam.setSamInterface(object : SamInterfaceTest.SamInterface1 {
override fun doWork(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转换的函数式接口
fun interface SamInterface {
fun test(value: Int)
fun
}
针对Kotlin函数式接口的转换:
class SamInterfaceTestKt {
fun testSam(obj: SamInterface) {
print("$obj")
}
}
// 测试
fun testKtSam(){
val samKt = 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++)代码实现。
external fun foo(x: Int): Double
二、Java 调 Kotlin
1、属性
一个Kotlin属性会编译为3部分Java元素
- 一个getter方法,名称通过加前缀
get
算出 - 一个setter方法,名称通过加前缀
set
算出(只适用于var
属性); - 一个私有的字段(field),其名字与Kotlin的属性名一样
如果Kotlin属性名以is开头,那么命名约定会发生一些变化:
- getter方法与属性名一样
- setter方法则是将is替换为set
- 一个私有的字段,其名字与Kotlin的属性名一样
举例说明:
Kotlin类:
class TestField {
val age: Int = 18
var userName: String = "Tom"
var isStudent: String = "yes"
}
编译生成的字节码对应的Java文件:
public final class TestField {
// final 类型的属性,只有getter方法,没有setter方法
private final int age = 18;
@NotNull
private String userName = "Tom";
@NotNull
private String isStudent = "yes";
public final int getAge() {
return this.age;
}
@NotNull
public final String getUserName() {
return this.userName;
}
public final void setUserName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.userName = var1;
}
@NotNull
public final String isStudent() {
return this.isStudent;
}
public final void setStudent(@NotNull String 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
package com.mei.ktx.test
/** * 在Kotlin文件中,声明一个类 */
class ClassInFile
/** * 在Kotlin文件中,声明一个方法 */
fun checkPhone(num: String): Boolean {
println("号码:$num")
return true
}
/** * 在Kotlin文件中,声明一个变量 */
var appName: String = "KotlinTest"
Kotlin编译器生成的对应的Java类:KotlinFileKt.java
public final class KotlinFileKt {
// Kotlin文件中声明的属性,变成了Java类中的私有静态属性
@NotNull
private static String appName = "KotlinTest";
// Kotlin文件中声明的方法,变成了Java类中的共有静态方法
public static final boolean checkPhone(@NotNull String num) {
Intrinsics.checkNotNullParameter(num, "num");
String var1 = "号码:" + num;
boolean var2 = false;
System.out.println(var1);
return true;
}
@NotNull
public static final String getAppName() {
return appName;
}
public static final void setAppName(@NotNull String var0) {
Intrinsics.checkNotNullParameter(var0, "<set-?>");
appName = var0;
}
}
Kotlin文件中声明的类,会生成一个独立的Java类,名称不变。
public final class ClassInFile {
}
所以Java调用Kotlin文件中的方法和属性的时候,需要通过对应的Java类名去调用:
public static void main(String[] args) {
// 通过类名直接调用
KotlinFileKt.checkPhone("123456");
System.out.println(KotlinFileKt.getAppName());
}
这里需要注意的是:Kotlin编译器自动生成的以Kt结尾的类,如:KotlinFileKt,是无法通过new关键字来创建对象的,因为在生成的字节码中没有构造方法的声明。
2-2、修改生成的类名
Kotlin文件所生成的Java类名,除了编译器默认生成的之外,还可以由自己指定,通过注解:@JvmName
如,Kotlin文件:
@file:JvmName("AppUtils") // 指定类名,需要在包名声明之前指定
package com.mei.ktx.test
/** * 在Kotlin文件中,声明一个类 */
class ClassInFile
/** * 在Kotlin文件中,声明一个方法 */
fun checkPhone(num: String): Boolean {
println("号码:$num")
return true
}
/** * 在Kotlin文件中,声明一个变量 */
var appName: String = "KotlinTest"
生成的Java类为:AppUtils
public final class AppUtils {
@NotNull
private static String appName = "KotlinTest";
public static final boolean checkPhone(@NotNull String num) {
Intrinsics.checkNotNullParameter(num, "num");
String var1 = "号码:" + num;
boolean var2 = false;
System.out.println(var1);
return true;
}
@NotNull
public static final String getAppName() {
return appName;
}
public static final void setAppName(@NotNull String var0) {
Intrinsics.checkNotNullParameter(var0, "<set-?>");
appName = var0;
}
}
注意:
- 使用注解:@file:JvmName(“类名”)
- 在文件包名声明之前,指定类名
2-3、类名冲突解决
通过上面介绍,我们知道可以为Kotlin文件指定类名,但如果多个相同包名下的Kotlin文件所指定的类名相同,这就会造成类重复定义,导致编译不过。这个时候就可以借助注解:@JvmMultifileClass
,把多个相同的类,合并成一个。
KotlinFile1.kt
@file:JvmName("LoginUtils")
@file:JvmMultifileClass
package com.mei.ktx.test
fun checkPwd(password: String): Boolean {
println("密码:$password")
return true
}
KotlinFile2.kt
@file:JvmName("LoginUtils")
@file:JvmMultifileClass
package com.mei.ktx.test
fun checkName(phone: String): Boolean {
println("号码:$phone")
return true
}
这样就没有冲突了,通过LoginUtils类就可以直接调用声明的方法:
public static void main(String[] args) {
LoginUtils.checkName("abc");
LoginUtils.checkPwd("123456");
}
注意:
-
在指定相同的类名的Kotlin文件中,都要加入该注解:@file:JvmMultifileClass
-
通常不建议自己指定类名。
3、实例字段
使用 @JvmField 注解对Kotlin中的属性进行标注时,表示它是一个实例字段(instance field),Kotlin编译器在编译的时候,就不会为这个属性生成对应的setter和getter方法,但可以直接访问这个属性,相当于是这个属性被声明称:public 了。
Kotlin类:
class Person {
var name: String = "张三"
@JvmField
var age: Int = 18
}
Java使用:
public static void main(String[] args) {
Person person = new Person();
System.out.println("name=" + person.getName() + ";age=" + person.age);
}
因为age被注解:@JvmField 修饰了,所以在Java类中,age字段就被当成时public类型的,可以自己访问,且没有生成对应的getter和setter方法。
从生成的Java类中也可以看出来:
public final class Person {
@NotNull
private String name = "张三";
@JvmField
public int age = 18; // 共有属性
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
}
使用限制:如果一个属性有幕后字段(backing field)、非私有、没有 open
/override
或者 const
修饰符并且不是被委托的属性,那么你可以用 @JvmField 注解该属性
感觉没啥用。
4、静态字段
4-1、Kotlin静态字段声明
Kotlin静态字段声明:在具名对象或伴生对象中声明的 Kotlin 属性,就是静态字段。它会在该具名对象或包含伴生对象的类中具有静态幕后字段。
如:伴生对象
class Person {
companion object {
var aliasName = "人" // 声明的静态字段
}
}
对应的Java类:
public final class Person {
private static String aliasName;
@NotNull
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public static final class Companion {
@NotNull
public final String getAliasName() {
return Person.aliasName;
}
public final void setAliasName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
Person.aliasName = var1;
}
}
}
从上面的Java代码也可以看出,这样声明的静态字段,是私有的静态字段,在Java中调用使用这样的静态字段,需要通过生成的伴生类:Companion 对象去获取和赋值,因为自动有生成getter和setter方法。
public static void main(String[] args) {
System.out.println("alias=" + Person.Companion.getAliasName());
}
4-2、静态字段公有化
通过上面声明的静态字段,默认是私有的静态字段,但我们可以通过如下方法,将私有字段变为公有字段:
- 使用
@JvmField
注解 修饰字段 - 使用 lateinit 修饰符 修饰字段
- 使用 const 修饰符 修饰字段
如:
class Person {
companion object {
// 使用const修饰
const val TAG = "Person"
// 使用lateinit修饰
lateinit var aliasName: String
// 使用注解
@JvmField
var age: Int = 18
}
}
通过上面三种方式修饰的静态字段,都是公有的静态字段,这个时候访问的时候,就可以直接通过类名去访问,不需要借助伴生类:Companion去访问。如:
public static void main(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中,在伴生对象中声明静态方法:
class Person {
companion object {
fun notStaticMethod() {
println("不是静态方法")
}
@JvmStatic
fun staticMethod() {
println("是静态方法")
}
}
}
上面代码,在伴生对象中声明了两个方法,通过注解:@JvmStatic 修饰的是静态方法,在Java中可以直接通过类名调用,也可以通过伴生对象调用。
@Test
public void test2(){
Person.Companion.staticMethod(); // 通过伴生对象调静态方法
Person.staticMethod();// 通过类名调用静态方法
Person.Companion.notStaticMethod();// 通过版本对象,调用非静态方法
}
@JvmStatic 注解也可以应用于对象或伴生对象的属性,使得该属性在该类中也有静态的 getter 和 setter 方法。
6、签名冲突
通过注解:@JvmName,可以解决函数签名冲突的问题。
6-1、泛型檫除导致的签名冲突
最突出的例子是由于类型擦除引发的:
fun List<String>.filterValid(): List<String> {
return arrayListOf("hello", "world")
}
fun List<Int>.filterValid(): List<Int> {
return arrayListOf(1, 2, 3)
}
在Kotlin文件中,定义上面两个扩展函数,是无法通过编译的,会提示报错:
即Kotlin在编译成字节码的时候,泛型会被檫除,导致两个方法在JVM看来,方法签名是一样的,都是:filterValid(Ljava/util/List;)Ljava/util/List;
这样JVM会认为这两个方法是同一个方法,但却被重复定义了。
解决办法是,通过注解 @JvmName 给方法重新指定一个名字,如:
fun List<String>.filterValid(): List<String> {
return arrayListOf("hello", "world")
}
@JvmName("filterValidInt") // 重新指定方法名称
fun List<Int>.filterValid(): List<Int> {
return arrayListOf(1, 2, 3)
}
val list=list(1,2,3)
list.
这样就可以编译通过了。
在 Kotlin 中它们可以用相同的名称 filterValid
来访问,而在 Java 中,它们分别是 filterValid
和 filterValidInt
。
Java中调用:
@Test
public void test3() {
List<String> stringList = new ArrayList<>();
System.out.println(ListExternalKt.filterValid(stringList));
List<Integer> integerList = new ArrayList<>();
System.out.println(ListExternalKt.filterValidInt(integerList));
}
Kotlin中调用:
fun main() {
val stringList = arrayListOf<String>()
println(stringList.filterValid())
val intList = arrayListOf<Int>()
println(intList.filterValid())// Kotlin调用的时候,直接就可以用方法名,而不是用重定义的方法名
}
输出:
6-2、属性的getter和setter方法与类中的现有方法冲突
同样的技巧也适用于属性 x
和函数 getX()
共存:
val x: Int
@JvmName("getXValue")
get() = 15
fun getX() = 10
如需在没有显式实现 getter 与 setter 的情况下更改属性生成的访问器方法的名称,可以使用**@get:JvmName** 与 @set:JvmName:
class Person {
@get:JvmName("getXValue")
@set:JvmName("setXValue")
var x: Int = 20
}
Java中调用:
@Test
public void test3() {
Person person = new Person();
person.setXValue(20);
}
7、生成重载
通常,如果你写一个有默认参数值的 Kotlin 函数,在Kotlin编译器生成的字节码中,只会有这么一个完整参数的方法,则Java调用这个方法的时候,需要传完整的参数,不可缺少。
如:Kotlin中定义了一个 Fruit 类,有一个两个参数的主构造函数,其中有一个参数有默认值
class Fruit constructor(var name: String, var type: Int = 1) {
// 有默认参数的构造函数
// 有默认参数的方法
fun setFuture(color: String, size: Int = 1) {
}
}
如果是在Kotlin中创建这个Fruit对象,则可以只传一个参数,默认参数可以不传。
但如果是在Java中创建这个Fruit对象,则两个参数都必须传,因为Java是不支持默认参数的。Fruit生成的字节码中,也只有这一个构造函数。如:
只传一个参数的话,Java编译不通过。
那可不可以让编译器帮我们生成多个重载的方法,当然是可以的。即可以使用 @JvmOverloads
注解来实现。
如:
给方法增加**@JvmOverloads**注解:
class Fruit @JvmOverloads constructor(var name: String, var type: Int = 1) {
@JvmOverloads
fun setFuture(color: String, size: Int = 1) {
}
}
这个时候在Java中创建Fruit对象,调用setFuture方法,都可以只传一个参数了:
本质原因是,Kotlin编译器在生成字节码的时候,为增加了 @JvmOverloads注解的方法,增加了多个重载方法,如查看Fruit类的Java代码如下:
public final class Fruit {
@NotNull
private String name;
private int type;
// 两个参数的setFuture方法
@JvmOverloads
public final void setFuture(@NotNull String color, int size) {
Intrinsics.checkNotNullParameter(color, "color");
}
// $FF: synthetic method
public static void setFuture$default(Fruit var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 2) != 0) {
var2 = 1;
}
var0.setFuture(var1, var2);
}
// 一个参数的setFuture方法
@JvmOverloads
public final void setFuture(@NotNull String color) {
setFuture$default(this, color, 0, 2, (Object)null);
}
// 两个参数的构造函数
@JvmOverloads
public Fruit(@NotNull String name, int type) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.type = type;
}
// $FF: synthetic method
public Fruit(String var1, int var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 2) != 0) {
var2 = 1;
}
this(var1, var2);
}
// 一个参数的构造函数
@JvmOverloads
public Fruit(@NotNull String name) {
this(name, 0, 2, (DefaultConstructorMarker)null);
}
}
正是因为Kotlin编译器帮我们生成了对用的重载方法,我们才可以调用。
8、受检异常
我们知道,Kotlin是没有受检异常的,所以Kotlin 函数的 Java 签名不会声明抛出异常。 于是如果我们有一个这样的 Kotlin 函数:
FileUtils文件:
fun writeToFile() {
println("写入文件")
throw IOException() // 在Kotlin方法中,抛出了一个IO异常
}
然后我们想要在 Java 中调用它并捕捉这个异常:
如果我们尝试去捕获这个IO异常,Java就会报错。原因是 writeToFile()未在 throws 列表中声明 IOException。所以在调用这个方法的时候,不能捕获到IOException。
为了解决Kotlin异常无法向上抛的问题,Kotlin提供了注解:@Throws
来解决这个问题
使用如下:
给需要向上抛异常的方法,增加**@Throws注解,并在注解上指明异常的类型,这里的类型是KClass**类型。
@Throws(IOException::class)
fun writeToFile() {
println("写入文件")
throw IOException() // 在Kotlin方法中,抛出了一个IO异常
}
通过注解**@Throws**,就可以把异常向上抛了,这样在Java调用Kotlin方法的时候,就可以捕获对应的异常了,如:
增加注解后,Java可以正常捕获到IOException异常了。
9、空安全
在Java调用Kotlin函数时,无法防止将null作为非空参数传递给函数。所以Kotlin为所有期望非空参数的public函数生成运行时检查。这样会在Java代码中立即出现NullPointerException异常。
fun checkPhone(num: String): Boolean {
println("号码:$num")
return true
}
Kotlin中的checkPhone方法,参数是非空类型的,在Java中调用这个方法时,如果传一个null的话,在运行时就会报空指针异常,如:
可以看到运行的时候,就报异常了。同时,在编译器也给我们提醒了,当传null的时候,报黄了。
如果Kotlin方法定义的时候,参数声明为可空类型,那么在Java中调用的时,传一个null,运行时就不会报空指针异常了:
// 参数声明为可空类型
fun checkPhone(num: String?): Boolean {
println("号码:$num")
return true
}
三、Android KTX使用
1、简述
Android KTX 是包含在 Android Jetpack 及其他 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")
inline fun SharedPreferences.edit(
commit: Boolean = false, // 是否通过commit方法提交数据,默认通过apply方法提交数据
action: SharedPreferences.Editor.() -> Unit // 表达式
) {
val editor = edit()// 获取Editor对象
action(editor) // 执行lambda表达式,即执行用户的代码
if (commit) {
editor.commit()// 提交数据
} else {
editor.apply()
}
}
通过上面的源码可以看出,edit方法,帮我们实现了需要重复编写的代码,让开发者只关注于自己的功能实现,从而减少代码量并提升效率。
2、项目中使用Android KTX
上面的针对SharedPreferences的扩展函数,定义在Android KTX Core核心库中,而Google针对不同的功能库,都提供了不同的扩展库,以更好的服务各个功能库,如:
扩展库名称 | 依赖 | 描述 |
---|---|---|
Core KTX | implementation “androidx.core:core-ktx:1.3.2” | 核心扩展库 |
Collection KTX | implementation “androidx.collection:collection-ktx:1.1.0” | 集合扩展库 |
Fragment KTX | implementation “androidx.fragment:fragment-ktx:1.3.1” | Fragment扩展库 |
Lifecycle KTX | implementation “androidx.lifecycle:lifecycle-runtime-ktx:2.3.0” | 声明周期扩展库 |
LiveData KTX | implementation “androidx.lifecycle:lifecycle-livedata-ktx:2.3.0” | LiveData扩展库 |
ViewModel KTX | implementation “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)、动画相关
针对动画的监听增加了一些扩展函数,避免了实现接口和实现方法,使用如下:
fun startAnimation() {
val animation = ValueAnimator.ofFloat(0f, 360f)
.setDuration(500)
animation.doOnCancel {
// 监听取消回调
}
animation.doOnEnd {
// 监听动画的结束
}
animation.start()
}
个人感觉比较鸡肋,每个扩展函数都会为动画增加一个监听对象,比如上面调用了两个扩展函数,就给animation对象增加了两个监听对象,感觉不划算,还增加了回调的成本。
(2)、Context相关
方法列表:https://developer.android.com/kotlin/ktx/extensions-list?hl=zh-cn#androidxcorecontent
比较实用的:解析自定义属性
Context.withStyledAttributes(set: AttributeSet? = null, attrs: IntArray, @AttrRes defStyleAttr: Int = 0, @StyleRes defStyleRes: Int = 0, block: TypedArray.() -> Unit)
使用:
class CusTextView @JvmOverloads constructor(
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](https://developer.android.com/reference/kotlin/androidx/core/graphics/package-summary?hl=zh-cn#(android.graphics.Canvas).withClip(android.graphics.Rect, 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这一系列的扩展函数,帮我们省去了画布的状态保存和恢复,并执行相应的操作,让开发者只关注于绘制本身,非常实用。
class CusTextView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {
override fun onDraw(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](https://developer.android.com/reference/kotlin/androidx/core/util/package-summary?hl=zh-cn#(android.util.SparseArray).getOrDefault(kotlin.Int, androidx.core.util.android.util.SparseArray.getOrDefault.T))(key: Int, defaultValue: T)
-
集合判空:SparseLongArray.isEmpty()
如:
fun test() {
val map = SparseArray<String>()
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()
其他的就不介绍了,大家可以去看官网,有详细的列举出每个包下的扩展API,忘记的时候也可以去查看一下,官网地址为:
今天的文章Kotlin与Java的相互调用详解分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/29721.html