互操作就是在Kotlin中可以调用其他编程语言的接口,只要它们开放了接口,Kotlin就可以调用其成员属性和成员方法,这是其他编程语言所无法比拟的。同时,在进行Java编程时也可以调用Kotlin中的API接口。
Kotlin调用Java
Kotlin在设计时就考虑了与Java的互操作性。可以从Kotlin中自然地调用现有的Java代码,在Java代码中也可以很顺利地调用Kotlin代码。例如,在Kotlin中调用Java的Util的list库。
基本的互操作行为如下:
属性读写
Kotlin可以自动识别Java中的getter/setter函数,而在Java中可以过getter/setter操作Kotlin属性。
循Java约定的getter和setter方法(名称以get开头的无参数方法和以set开头的单参数方法)在Kotlin中表示为属性。如果Java类只有一个setter,那么它在Kotlin中不会作为属性可见,因为Kotlin目前不支持只写(set-only)属性。
空安全类型
Kotlin的空安全类型的原理是,Kotlin在编译过程中会增加一个函数调用,对参数类型或者返回类型进行控制,开发者可以在开发时通过注解@Nullable和@NotNull方式来限制Java中空值异常。
Java中的任何引用都可能是null,这使得Kotlin对来自Java的对象进行严格的空安全检查是不现实的。Java声明的类型在Kotlin中称为平台类型,并会被特别对待。对这种类型的空检查要求会放宽,因此对它们的安全保证与在Java中相同。
当调用平台类型变量的方法时,Kotlin不会在编译时报告可空性错误,但是在运行时调用可能会失败,因为空指针异常。
平台类型是不可标识的,这意味着不能在代码中明确地标识它们。当把一个平台值赋给一个Kotlin变量时,可以依赖类型推断(该变量会具有所推断出的平台类型,如上例中item所具有的类型),或者选择我们所期望的类型(可空的或非空类型均可)。
如果选择非空类型,编译器会在赋值时触发一个断言,这样可以防止Kotlin的非空变量保存空值。当把平台值传递给期待非空值等的Kotlin函数时,也会触发一个断言。总的来说,编译器尽力阻止空值的传播(由于泛型的原因,有时这不可能完全消除)。
平台类型标识法
如上所述,平台类型不能在程序中显式表述,因此在语言中没有相应语法。 然而,编译器和 IDE 有时需要(在错误信息中、参数信息中等)显示他们,Koltin提供助记符来表示他们:
T! 表示“T 或者 T?”;
(Mutable)Collection! 表示“可以可变或不可变、可空或不可空的 T 的 Java 集合”;
Array<(out) T>! 表示“可空或者不可空的 T(或 T 的子类型)的 Java 数组”。
可空注解
由于泛型的原因,Kotlin在编译时可能出现空异常,而使用空注解可以有效的解决这一情况。编译器支持多种可空性注解:
JetBrains:org.jetbrains.annotations 包中的
@Nullable 和 @NotNull;
Android:com.android.annotations 和 android.support.annotations;
JSR-305:javax.annotation;
FindBugs:edu.umd.cs.findbugs.annotations;
Eclipse:org.eclipse.jdt.annotation;
Lombok:lombok.NonNull;
JSR-305 支持
在JSR-305中,定义的 @Nonnull 注解来表示 Java 类型的可空性。
如果 @Nonnull(when = ...) 值为 When.ALWAYS,那么该注解类型会被视为非空;When.MAYBE 与 When.NEVER 表示可空类型;而 When.UNKNOWN 强制类型为平台类型。
可针对 JSR-305 注解编译库,但不需要为库的消费者将注解构件(如 jsr305.jar)指定为编译依赖。Kotlin 编译器可以从库中读取 JSR-305 注解,并不需要该注解出现在类路径中。
自 Kotlin 1.1.50 起, 也支持自定义可空限定符(KEEP-79)
类型限定符
如果一个注解类型同时标注有 @TypeQualifierNickname 与 JSR-305 @Nonnull(或者它的其他别称,如 @CheckForNull),那么该注解类型自身将用于 检索精确的可空性,且具有与该可空性注解相同的含义。
类型限定符默认值
@TypeQualifierDefault 引入应用时在所标注元素的作用域内定义默认可空性的注解。这些注解类型应自身同时标注有 @Nonnull(或其别称)与 @TypeQualifierDefault(...) 注解, 后者带有一到多个 ElementType 值。
ElementType.METHOD 用于方法的返回值;
ElementType.PARAMETER 用于值参数;
ElementType.FIELD 用于字段;
ElementType.TYPE_USE(自 1.1.60 起)适用于任何类型,包括类型参数、类型参数的上界与通配符类型。
当类型并未标注可空性注解时使用默认可空性,并且该默认值是由最内层标注有带有与所用类型相匹配的 ElementType 的类型限定符默认注解的元素确定。
也支持包级的默认可空性:
@UnderMigration 注解
库的维护者可以使用 @UnderMigration 注解(在单独的构件 kotlin-annotations-jvm 中提供)来定义可为空性类型限定符的迁移状态。
@UnderMigration(status = ...) 中的状态值指定了编译器如何处理 Kotlin 中注解类型的不当用法(例如,使用 @MyNullable 标注的类型值作为非空值):
MigrationStatus.STRICT 使注解像任何纯可空性注解一样工作,即对不当用法报错并影响注解声明内的类型在 Kotlin中的呈现;
对于 MigrationStatus.WARN,不当用法报为警告而不是错误; 但注解声明内的类型仍是平台类型;
MigrationStatus.IGNORE 则使编译器完全忽略可空性注解。
库的维护者还可以将 @UnderMigration 状态添加到类型限定符别称与类型限定符默认值中。例如:
注意:可空性注解的迁移状态并不会从其类型限定符别称继承,而是适用于默认类型限定符的用法。如果默认类型限定符使用类型限定符别称,并且它们都标注有 @UnderMigration,那么使用默认类型限定符的状态。
返回void的方法
如果在Java中返回void,那么Kotlin返回的就是Unit。如果在调用时返回void,那么Kotlin会事先识别该返回值为void。
注解的使用
@JvmField是Kotlin和Java互相操作属性经常遇到的注解;@JvmStatic是将对象方法编译成Java静态方法;@JvmOverloads主要是Kotlin定义默认参数生成重载方法;@file:JvmName指定Kotlin文件编译之后生成的类名。
NoArg和AllOpen
数据类本身属性没有默认的无参数的构造方法,因此Kotlin提供一个NoArg插件,支持JPA注解,如@Entity。AllOpen是为所标注的类去掉final,目的是为了使该类允许被继承,且支持Spring注解,如@Componet;支持自定义注解类型,如@Poko。
泛型
Kotlin 的泛型与 Java 有点不同,读者可以具体参考泛型章节。Kotlin中的通配符“”代替Java中的“?”;协变和逆变由Java中的extends和super变成了out和in,如ArrayList;在Kotlin中没有Raw类型,如Java中的List对应于Kotlin就是List<>。
与Java一样,Kotlin在运行时不保留泛型,也就是对象不携带传递到它们的构造器中的类型参数的实际类型,即ArrayList()和ArrayList()是不能区分的。这使得执行is检查不可能照顾到泛型,Kotlin只允许is检查星投影的泛型类型。
Java数组
与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把一个 Array 赋值给一个 Array, 从而避免了可能的运行时故障。Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法, 但是对于 Java 方法,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。
Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特化的类(IntArray、 DoubleArray、 CharArray 等等)来处理这种情况。 它们与 Array 类无关,并且会编译成 Java 原生类型数组以获得最佳性能。
例如,假设有一个接受 int 数组索引的 Java 方法。
在 Kotlin 中调用该方法时,你可以这样传递一个原生类型的数组。
当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引入任何开销。
即使当我们使用索引定位时,也不会引入任何开销:
最后,in-检测也没有额外开销:
Java 可变参数
Java 类有时声明一个具有可变数量参数(varargs)的方法来使用索引。
在这种情况下,你需要使用展开运算符 * 来传递 IntArray。
目前,无法传递 null 给一个声明为可变参数的方法。
SAM转换
就像Java 8一样,Kotlin支持SAM转换,这意味着Kotlin函数字面值可以被自动转换成只有一个非默认方法的Java接口的实现,只要这个方法的参数类型能够与这个Kotlin函数的参数类型相匹配就行。
首先使用Java创建一个SAMInJava类,然后通过Kotlin调用Java中的接口。
然后在Kotlin中调用该Java接口。
运行结果为:
如果Java类有多个接受函数式接口的方法,那么可以通过使用将Lambda表达式转换为特定的SAM类型的适配器函数来选择需要调用的方法。
注意:SAM转换只适用于接口,而不适用于抽象类,即使这些抽象类只有一个抽象方法。此功能只适用于Java互操作;因为Kotlin具有合适的函数类型,所以不需要将函数自动转换为Kotlin接口的实现,因此不受支持。
除此之外,Kotlin调用Java还有很多的内容,读者可以通过下面的链接来了解:Kotlin调用Java
Java调用Kotlin
Java 可以轻松调用 Kotlin 代码。
属性
Kotlin属性会被编译成以下Java元素:
getter方法,其名称通过加前缀get得到;
setter方法,其名称通过加前缀set得到(只适用于var属性);
私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。
例如,将Kotlin变量编译成Java中的变量声明。
如果属性名称是以is开头的,则使用不同的名称映射规则:getter的名称与属性名称相同,并且setter的名称是通过将is替换成set获得的。例如,对于属性isOpen,其getter会称作isOpen(),而其setter会称作setOpen()。这一规则适用于任何类型的属性,并不仅限于Boolean。
包级函数
例如,在org.foo.bar 包内的 example.kt 文件中声明的所有的函数和属性,包括扩展函数, 该 类会编译成一个名为 org.foo.bar.ExampleKt 的 Java 类的静态方法。
首先,新建一个ExampleKt.kt的文件,并新建一个bar函数:
然后,在Java中调用这个函数。
当然,可以使用@JvmName注解修改所生成的Java类的类名。例如:
那么在Java调用时就需要修改类名。例如:
在多个文件中生成相同的Java类名(包名相同并且类名相同或者有相同的@JvmName注解)通常是错误的。然而,编译器能够生成一个单一的Java外观类,它具有指定的名称且包含来自于所有文件中具有该名称的所有声明。要生成这样的外观,请在所有的相关文件中使用@JvmMultifileClass注解。
实例字段
如果需要在Java中将Kotlin属性作为字段暴露,那么就需要使用@JvmField注解对其进行标注。使用@JvmField注解标注后,该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(Backing Field)、非私有的、没有open/override或者const修饰符,并且不是被委托的属性,那么可以使用@JvmField注解该属性。
首先,新建一个kt类,并添加如下代码。
然后在Java中调用该代码,
延迟初始化的属性(在Java中)也会暴露为字段, 该字段的可见性与 lateinit 属性的 setter 相同。
静态字段
在命名对象或伴生对象时,声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中包含静态幕后字段。通常这些字段是私有的,但可以通过以下方式之一暴露出来。
@JvmField 注解;
lateinit 修饰符;
const 修饰符。
使用 @JvmField 标注的属性,可以使其成为与属性本身具有相同可见性的静态字段。例如:
然后,在Java代码中调用属性。
在命名对象或者伴生对象中的一个延迟初始化的属性具有与属性 setter 相同可见性的静态幕后字段。
然后,在Java中使用该字段的属性。
用 const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段,首先新建一个kt文件。
然后,在Java中可以直接调用该属性即可。
静态方法
Kotlin将包级函数表示为静态方法。如果对这些函数使用@JvmStatic进行标注,那么Kotlin还可以为在命名对象或伴生对象中定义的函数生成静态方法。如果使用该注解,那么编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。例如:
现在,foo()在Java中是静态的,而bar()不是静态的。
对于命名对象,也存在同样的规律。
在 Java 中使用。
@JvmStatic 注解也可以应用于对象或伴生对象的属性, 使其 getter 和 setter 方法在该对象或包含该伴生对象的类中是静态成员。
可见性
Kotlin的可见性以下列方式映射到Java代码中。
private 成员编译成 private 成员;
private 的顶层声明编译成包级局部声明;
protected 保持 protected(注意 Java 允许访问同一个包中其他类的受保护成员, 而 Kotlin 不能,所以Java 类会访问更广泛的代码);
internal 声明会成为 Java 中的 public。internal 类的成员会通过名字修饰,使其更难以在 Java 中意外使用到,并且根据 Kotlin 规则使其允许重载相同签名的成员而互不可见;
public 保持 public。
KClass
有时你需要调用有 KClass 类型参数的 Kotlin 方法。 因为没有从 Class 到 KClass 的自动转换,所以你必须通过调用 Class.kotlin 扩展属性的等价形式来手动进行转换。例如:
签名冲突
有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称,最突出的例子是由于类型擦除引发的。
这两个函数不能同时定义在一个类中,因为它们的 JVM 签名是一样的。如果我们真的希望它们在 Kotlin 中使用相同的名称,可以使用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数。例如:
在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。同样的技巧也适用于属性中。例如:
生成重载
通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。该注解可以用于构造函数、静态方法中,但不能用于抽象方法和在接口中定义的方法。
对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 。
请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数,此时就算没有 @JvmOverloads 注解也有效。
受检异常
如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常, 于是如果我们有一个这样的 Kotlin 函数。首先,新建一个kt文件。
然后,在 Java 中调用它的时候,需要使用try{}catch{}来捕捉这个异常。
因为 foo() 没有声明 IOException,我们从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws 注解。
空安全性
当从Java中调用Kotlin函数时,没有任何方法可以阻止Kotlin中的空值传入。Kotlin在JVM虚拟机中运行时会检查所有的公共函数,可以检查非空值,这时候就可以通过NullPointerException得到Java中的非空值代码。
型变的泛型
当 Kotlin 的类使用了声明处型变时,可以通过两种方式从Java代码中看到它们的用法。让我们假设我们有以下类和两个使用它的函数:
将这两个函数转换成Java代码如下:
问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived("s")),但是在 Java 中是行不通的,因为在 Java 中类 Box 在其泛型参数 T 上是不型变的,于是 Box 并不是 Box 的子类。 要使其在 Java 中工作,我们按以下这样定义 unboxBase。
这里我们使用 Java 的通配符类型(? extends Base)来通过使用处型变来模拟声明处型变,因为在 Java 中只能这样。
当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中工作,对于协变定义的 Box 我们生成 Box 作为 Box extends Super> (或者对于逆变定义的 Foo 生成 Foo super Bar>)。当它是一个返回值时, 我们不生成通配符,因为否则 Java 客户端将必须处理它们(并且它违反常用 Java 编码风格)。因此,我们的示例中的对应函数实际上翻译如下:
注意:当参数类型是 final 时,生成通配符通常没有意义,所以无论在什么地方 Box 始终转换为 Box。如果我们在默认不生成通配符的地方需要通配符,我们可以使用 @JvmWildcard 注解:
另一方面,如果我们根本不需要默认的通配符转换,我们可以使用@JvmSuppressWildcards。
注意:@JvmSuppressWildcards 不仅可用于单个类型参数,还可用于整个声明(如函数或类),从而抑制其中的所有通配符。
Nothing 类型
类型 Nothing 是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行,以为这种类型不能在 Java 中被准确表示。这就是为什么在使用 Nothing 参数的地方 Kotlin 生成一个原始类型:
更多精彩文章请关注微信公众号“云栖社区”或扫描下方二维码