Kotlin与Java互操作
在Kotlin中操作Java
示例代码:
Person类
package com.leofight.kotlin10;
public class Person {
private String name;
private boolean married;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMarried() {
return married;
}
public void setMarried(boolean married) {
this.married = married;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
fun main(args: Array<String>) {
val list = ArrayList<String>()
list.add("hello")
list.add("world")
list.add("hello world")
for (item in list) {
println(item)
}
println("------")
for (i in 0 until list.size) {
println(list[i])
}
println("-------")
val person = Person()
person.age = 20
person.isMarried = false
person.name = "zhangsan"
println(person.age)
println(person.isMarried)
println(person.name)
println("------")
}
在Java中,所有引用都可能为null,然而在Kotlin中,对null是有着严格的检查与限制的,这就使得某个来自于Java的引用在Kotlin中变得不再适合;基于这个原因,在Kotlin中,将来自于Java的声明类型称为平台类型(Platform Types)。
对于这种类型(平台类型)来说,Kotlin的null检查就会得到一定的缓和,变得不再那么严格了。这样就使得空安全的语义要求变的与Java一致。
当我们调用平台类型引用的方法时,Kotlin就不会在编译期间施加空安全的检查,使得编译可以正常通过;但是在运行期间则 可能抛出异常,因为平台类型引用值就可能为null。
fun main(args: Array<String>) {
val list2 = ArrayList<String>()
//list2.add("hello")
val size = list2.size
val item = list2[0]
println(size)
println(item)
val s: String? = item //允许,总是可以的
val s2: String = item //允许,不过可能会在运行期间失败
}
如果我们使用了不可空类型,编译器会在赋值时生成一个断言。这会防止Kotlin的不可空变量持有null值;同样,这一点也适用于Kotlin方法参数传递,我们在将一个平台类型值传递给方法的一个不可空参数时,也会生成一个断言。
总体来说,Kotlin会竭尽所能防止null的赋值蔓延到程序的其他地方,而是在发生问题之处就立刻通过断言来解决。
数组(Array)
Kotlin中数组是不变的(相对于协变与逆变来说),这一点与Java存在明显的不同。这意味着,我们无法将一个Array<String>赋给Array<Any>,这样就可以防止可能出现的运行期异常。
Kotlin提供了原生类型数组来避免自动装箱与拆箱带来的成本:IntArray、DoubleArray、CharArray...
示例代码:
public class MyArray {
public void myArrayMethod(int[] args) {
}
}
fun main(args: Array<String>) {
val myArray = MyArray()
val intArray = intArrayOf(1, 2, 3, 4)
myArray.myArrayMethod(intArray)
println("---------")
//当编译为JVM字节码时,编译器会优化对于数组的访问,使之不会产出额外的成本
val array = arrayOf(1, 2, 3, 4)
array[0] = array[0] * 2 //并不会调用set或者get方法
for (x in array) {
println(x)
}
}
可变参数
public class MyVarargs {
public void myMthod(String... strings) {
}
}
fun main(args: Array<String>) {
val myVarargs = MyVarargs()
val stringArrays = arrayOf("hello", "world", "hello world")
myVarargs.myMthod(*stringArrays) //spread operation *
}
受检异常
在 Kotlin 中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:
public class MyException {
public void myMethod() throws IOException {
throw new IOException("I/O异常");
}
}
fun main(args: Array<String>) {
/* val myException = MyException();
myException.myMethod();*/
println("--------")
//要取得对象的 Java 类
val clazz = MyException()::class.java
println(clazz)
println("---------")
println(MyException().javaClass)
}
Java操作Kotlin
属性(properties)
一个Kotlin属性会被编译为3部分Java元素
1.一个getter方法
2.一个setter方法
3.一个私有的字段(field),其名字与Kotlin的属性名一样
如果Kotlin属性名以is开头,那么命名约定会发生一些变化。
1.getter方法名与属性名一样
2.setter方法名则是将is替换为set
这种规则适用于任何类型,而不单单是Boolean类型。
例如,var firstName: String
编译成以下 Java 声明:
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
示例
class MyClass
fun test() {
println("hello world")
}
var str: String = "hello"
public class HelloJava2 {
public static void main(String[] args) {
//我们无法通过new关键字来创建Kotlin编译器自动生成的以Kt结尾的类的实例。
//因为在生成的字节码中没有不带参数的构造方法。
//HelloKotlin2Kt helloKotlin2Kt = new HelloKotlin2Kt();
MyClass myClass = new MyClass();
HelloKotlin2Kt.setStr("welcome");
System.out.println(HelloKotlin2Kt.getStr());
HelloKotlin2Kt.test();
}
}
输出
welcome
hello world
包级函数
在 org.foo.bar 包内的 example.kt 文件中声明的所有的函数和属性,包括扩展函数, 都编译成一个名为 org.foo.bar.ExampleKt 的 Java 类的静态方法。
// example.kt
package demo
class Foo
fun bar() { …… }
// Java
new demo.Foo();
demo.ExampleKt.bar();
可以使用 @JvmName 注解修改生成的 Java 类的类名:
@file:JvmName("DemoUtils")
package demo
class Foo
fun bar() { ... }
// Java
new demo.Foo();
demo.DemoUtils.bar();
如果多个文件中生成了相同的 Java 类名(包名相同并且类名相同或者有相同的 @JvmName 注解)通常是错误的。然而,编译器能够生成一个单一的 Java 外观类,它具有指定的名称且包含来自所有文件中具有该名称的所有声明。 要启用生成这样的外观,请在所有相关文件中使用 @JvmMultifileClass 注解。
// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun foo() { ... }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun bar() { ... }
// Java
demo.Utils.foo();
demo.Utils.bar();
实例字段
如果需要在 Java 中将 Kotlin 属性作为字段暴露,那就需要使用 @JvmField 注解对其标注。 该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(backing field)、非私有、没有 open /override 或者 const 修饰符并且不是被委托的属性,那么你可以用 @JvmField 注解该属性。
class C(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(C c) {
return c.ID;
}
}
延迟初始化的属性(在Java中)也会暴露为字段。 该字段的可见性与 lateinit
属性的 setter 相同。
静态字段
在命名对象或伴生对象中声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中具有静态幕后字段。
通常这些字段是私有的,但可以通过以下方式之一暴露出来:
- @JvmField 注解;
- lateinit 修饰符;
- const 修饰符。
使用 @JvmField 标注这样的属性使其成为与属性本身具有相同可见性的静态字段。
class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
// Java
Key.COMPARATOR.compare(key1, key2);
// Key 类中的 public static final 字段
在命名对象或者伴生对象中的一个延迟初始化的属性具有与属性 setter 相同可见性的静态幕后字段。
object Singleton {
lateinit var provider: Provider
}
// Java
Singleton.provider = new Provider();
// 在 Singleton 类中的 public static 非-final 字段
用 const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段:
// 文件 example.kt
object Obj {
const val CONST = 1
}
class C {
companion object {
const val VERSION = 9
}
}
const val MAX = 239
在Java中
int c = Obj.CONST;
int d = ExampleKt.MAX;
int v = C.VERSION;
静态方法
如上所述,Kotlin 将包级函数表示为静态方法。 Kotlin 还可以为命名对象或伴生对象中定义的函数生成静态方法,如果你将这些函数标注为 @JvmStatic 的话。 如果你使用该注解,编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。 例如:
class C {
companion object {
@JvmStatic fun foo() {}
fun bar() {}
}
}
现在,foo() 在 Java 中是静态的,而 bar() 不是:
C.foo(); // 没问题
C.bar(); // 错误:不是一个静态方法
C.Companion.foo(); // 保留实例方法
C.Companion.bar(); // 唯一的工作方式
对于命名对象也同样:
object Obj {
@JvmStatic fun foo() {}
fun bar() {}
}
在Java中
Obj.foo(); // 没问题
Obj.bar(); // 错误
Obj.INSTANCE.bar(); // 没问题,通过单例实例调用
Obj.INSTANCE.foo(); // 也没问题
@JvmStatic 注解也可以应用于对象或伴生对象的属性, 使其 getter 和 setter 方法在该对象或包含该伴生对象的类中是静态成员。
用 @JvmName 解决签名冲突
有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称。 最突出的例子是由于类型擦除引发的:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
这两个函数不能同时定义,因为它们的 JVM 签名是一样的:filterValid(Ljava/util/List;)Ljava/util/List;。 如果我们真的希望它们在 Kotlin 中用相同名称,我们需要用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。
同样的技巧也适用于属性 x 和函数 getX() 共存:
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
如需在没有显式实现 getter 与 setter 的情况下更改属性生成的访问器方法的名称,可以使用 @get:JvmName 与 @set:JvmName:
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23
生成重载
通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。
该注解也适用于构造函数、静态方法等。它不能用于抽象方法,包括在接口中定义的方法。
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { …… }
}
对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 :
// 构造函数:
Foo(int x, double y)
Foo(int x)
// 方法
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }
请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数。这就算没有 @JvmOverloads
注解也有效。