Kotlin与Java互操作

互操作就是在Kotlin中可以调用其他编程语言的接口,只要它们开放了接口,Kotlin就可以调用其成员属性和成员方法,这是其他编程语言所无法比拟的。同时,在进行Java编程时也可以调用Kotlin中的API接口。


Kotlin调用Java


Kotlin在设计时就考虑了与Java的互操作性。可以从Kotlin中自然地调用现有的Java代码,在Java代码中也可以很顺利地调用Kotlin代码。例如,在Kotlin中调用Java的Util的list库。


Kotlin与Java互操作


基本的互操作行为如下:


属性读写


Kotlin可以自动识别Java中的getter/setter函数,而在Java中可以过getter/setter操作Kotlin属性。


Kotlin与Java互操作


循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与Java互操作


当调用平台类型变量的方法时,Kotlin不会在编译时报告可空性错误,但是在运行时调用可能会失败,因为空指针异常。


Kotlin与Java互操作


平台类型是不可标识的,这意味着不能在代码中明确地标识它们。当把一个平台值赋给一个Kotlin变量时,可以依赖类型推断(该变量会具有所推断出的平台类型,如上例中item所具有的类型),或者选择我们所期望的类型(可空的或非空类型均可)。


Kotlin与Java互操作


如果选择非空类型,编译器会在赋值时触发一个断言,这样可以防止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),那么该注解类型自身将用于 检索精确的可空性,且具有与该可空性注解相同的含义。


Kotlin与Java互操作


类型限定符默认值


@TypeQualifierDefault 引入应用时在所标注元素的作用域内定义默认可空性的注解。这些注解类型应自身同时标注有 @Nonnull(或其别称)与 @TypeQualifierDefault(...) 注解, 后者带有一到多个 ElementType 值。


  • ElementType.METHOD 用于方法的返回值;


  • ElementType.PARAMETER 用于值参数;


  • ElementType.FIELD 用于字段;


  • ElementType.TYPE_USE(自 1.1.60 起)适用于任何类型,包括类型参数、类型参数的上界与通配符类型。


当类型并未标注可空性注解时使用默认可空性,并且该默认值是由最内层标注有带有与所用类型相匹配的 ElementType 的类型限定符默认注解的元素确定。


Kotlin与Java互操作


也支持包级的默认可空性:


Kotlin与Java互操作


@UnderMigration 注解


库的维护者可以使用 @UnderMigration 注解(在单独的构件 kotlin-annotations-jvm 中提供)来定义可为空性类型限定符的迁移状态。


@UnderMigration(status = ...) 中的状态值指定了编译器如何处理 Kotlin 中注解类型的不当用法(例如,使用 @MyNullable 标注的类型值作为非空值):


  • MigrationStatus.STRICT 使注解像任何纯可空性注解一样工作,即对不当用法报错并影响注解声明内的类型在 Kotlin中的呈现;


  • 对于 MigrationStatus.WARN,不当用法报为警告而不是错误; 但注解声明内的类型仍是平台类型;


  • MigrationStatus.IGNORE 则使编译器完全忽略可空性注解。


库的维护者还可以将 @UnderMigration 状态添加到类型限定符别称与类型限定符默认值中。例如:


Kotlin与Java互操作


注意:可空性注解的迁移状态并不会从其类型限定符别称继承,而是适用于默认类型限定符的用法。如果默认类型限定符使用类型限定符别称,并且它们都标注有 @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检查星投影的泛型类型。


Kotlin与Java互操作


Java数组


与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把一个 Array 赋值给一个 Array, 从而避免了可能的运行时故障。Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法, 但是对于 Java 方法,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。


Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特化的类(IntArray、 DoubleArray、 CharArray 等等)来处理这种情况。 它们与 Array 类无关,并且会编译成 Java 原生类型数组以获得最佳性能。


例如,假设有一个接受 int 数组索引的 Java 方法。


Kotlin与Java互操作


在 Kotlin 中调用该方法时,你可以这样传递一个原生类型的数组。


Kotlin与Java互操作


当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引入任何开销。


Kotlin与Java互操作


即使当我们使用索引定位时,也不会引入任何开销:


Kotlin与Java互操作


最后,in-检测也没有额外开销:


Kotlin与Java互操作


Java 可变参数


Java 类有时声明一个具有可变数量参数(varargs)的方法来使用索引。


Kotlin与Java互操作


在这种情况下,你需要使用展开运算符 * 来传递 IntArray。


Kotlin与Java互操作


目前,无法传递 null 给一个声明为可变参数的方法。


SAM转换


就像Java 8一样,Kotlin支持SAM转换,这意味着Kotlin函数字面值可以被自动转换成只有一个非默认方法的Java接口的实现,只要这个方法的参数类型能够与这个Kotlin函数的参数类型相匹配就行。


首先使用Java创建一个SAMInJava类,然后通过Kotlin调用Java中的接口。


Kotlin与Java互操作


然后在Kotlin中调用该Java接口。


Kotlin与Java互操作


运行结果为:


Kotlin与Java互操作


如果Java类有多个接受函数式接口的方法,那么可以通过使用将Lambda表达式转换为特定的SAM类型的适配器函数来选择需要调用的方法。


Kotlin与Java互操作


注意:SAM转换只适用于接口,而不适用于抽象类,即使这些抽象类只有一个抽象方法。此功能只适用于Java互操作;因为Kotlin具有合适的函数类型,所以不需要将函数自动转换为Kotlin接口的实现,因此不受支持。


除此之外,Kotlin调用Java还有很多的内容,读者可以通过下面的链接来了解:Kotlin调用Java


Java调用Kotlin


Java 可以轻松调用 Kotlin 代码。


属性


Kotlin属性会被编译成以下Java元素:


  • getter方法,其名称通过加前缀get得到;


  • setter方法,其名称通过加前缀set得到(只适用于var属性);


  • 私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。


例如,将Kotlin变量编译成Java中的变量声明。


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函数:


Kotlin与Java互操作


然后,在Java中调用这个函数。


Kotlin与Java互操作


当然,可以使用@JvmName注解修改所生成的Java类的类名。例如:


Kotlin与Java互操作


那么在Java调用时就需要修改类名。例如:


Kotlin与Java互操作


在多个文件中生成相同的Java类名(包名相同并且类名相同或者有相同的@JvmName注解)通常是错误的。然而,编译器能够生成一个单一的Java外观类,它具有指定的名称且包含来自于所有文件中具有该名称的所有声明。要生成这样的外观,请在所有的相关文件中使用@JvmMultifileClass注解。


Kotlin与Java互操作


实例字段


如果需要在Java中将Kotlin属性作为字段暴露,那么就需要使用@JvmField注解对其进行标注。使用@JvmField注解标注后,该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(Backing Field)、非私有的、没有open/override或者const修饰符,并且不是被委托的属性,那么可以使用@JvmField注解该属性。


首先,新建一个kt类,并添加如下代码。


Kotlin与Java互操作


然后在Java中调用该代码,


Kotlin与Java互操作


延迟初始化的属性(在Java中)也会暴露为字段, 该字段的可见性与 lateinit 属性的 setter 相同。


静态字段


在命名对象或伴生对象时,声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中包含静态幕后字段。通常这些字段是私有的,但可以通过以下方式之一暴露出来。


  • @JvmField 注解;


  • lateinit 修饰符;


  • const 修饰符。


使用 @JvmField 标注的属性,可以使其成为与属性本身具有相同可见性的静态字段。例如:


Kotlin与Java互操作


然后,在Java代码中调用属性。


Kotlin与Java互操作


在命名对象或者伴生对象中的一个延迟初始化的属性具有与属性 setter 相同可见性的静态幕后字段。


Kotlin与Java互操作


然后,在Java中使用该字段的属性。


Kotlin与Java互操作


用 const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段,首先新建一个kt文件。


Kotlin与Java互操作


然后,在Java中可以直接调用该属性即可。


Kotlin与Java互操作


静态方法


Kotlin将包级函数表示为静态方法。如果对这些函数使用@JvmStatic进行标注,那么Kotlin还可以为在命名对象或伴生对象中定义的函数生成静态方法。如果使用该注解,那么编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。例如:


Kotlin与Java互操作


现在,foo()在Java中是静态的,而bar()不是静态的。


Kotlin与Java互操作


对于命名对象,也存在同样的规律。


Kotlin与Java互操作


在 Java 中使用。


Kotlin与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与Java互操作


签名冲突


有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称,最突出的例子是由于类型擦除引发的。


Kotlin与Java互操作


这两个函数不能同时定义在一个类中,因为它们的 JVM 签名是一样的。如果我们真的希望它们在 Kotlin 中使用相同的名称,可以使用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数。例如:


Kotlin与Java互操作


在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。同样的技巧也适用于属性中。例如:


Kotlin与Java互操作


生成重载


通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。该注解可以用于构造函数、静态方法中,但不能用于抽象方法和在接口中定义的方法。


Kotlin与Java互操作


对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 。


Kotlin与Java互操作


请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数,此时就算没有 @JvmOverloads 注解也有效。


受检异常


如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常, 于是如果我们有一个这样的 Kotlin 函数。首先,新建一个kt文件。


Kotlin与Java互操作


然后,在 Java 中调用它的时候,需要使用try{}catch{}来捕捉这个异常。


Kotlin与Java互操作


因为 foo() 没有声明 IOException,我们从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws 注解。


Kotlin与Java互操作


空安全性


当从Java中调用Kotlin函数时,没有任何方法可以阻止Kotlin中的空值传入。Kotlin在JVM虚拟机中运行时会检查所有的公共函数,可以检查非空值,这时候就可以通过NullPointerException得到Java中的非空值代码。


型变的泛型


当 Kotlin 的类使用了声明处型变时,可以通过两种方式从Java代码中看到它们的用法。让我们假设我们有以下类和两个使用它的函数:


Kotlin与Java互操作


将这两个函数转换成Java代码如下:


Kotlin与Java互操作


问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived("s")),但是在 Java 中是行不通的,因为在 Java 中类 Box 在其泛型参数 T 上是不型变的,于是 Box 并不是 Box 的子类。 要使其在 Java 中工作,我们按以下这样定义 unboxBase。


Kotlin与Java互操作


这里我们使用 Java 的通配符类型(? extends Base)来通过使用处型变来模拟声明处型变,因为在 Java 中只能这样。


当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中工作,对于协变定义的 Box 我们生成 Box 作为 Box<? extends Super> (或者对于逆变定义的 Foo 生成 Foo<? super Bar>)。当它是一个返回值时, 我们不生成通配符,因为否则 Java 客户端将必须处理它们(并且它违反常用 Java 编码风格)。因此,我们的示例中的对应函数实际上翻译如下:


Kotlin与Java互操作


注意:当参数类型是 final 时,生成通配符通常没有意义,所以无论在什么地方 Box 始终转换为 Box。如果我们在默认不生成通配符的地方需要通配符,我们可以使用 @JvmWildcard 注解:


Kotlin与Java互操作


另一方面,如果我们根本不需要默认的通配符转换,我们可以使用@JvmSuppressWildcards。


Kotlin与Java互操作


注意:@JvmSuppressWildcards 不仅可用于单个类型参数,还可用于整个声明(如函数或类),从而抑制其中的所有通配符。


Nothing 类型


类型 Nothing 是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行,以为这种类型不能在 Java 中被准确表示。这就是为什么在使用 Nothing 参数的地方 Kotlin 生成一个原始类型:


Kotlin与Java互操作


精彩活动

新年萌礼!

点此互动赢取阿里云新年礼盒!

内含阿里云吉祥物小ET!


-END-Kotlin与Java互操作

云栖社区

ID:yunqiinsight

云计算丨互联网架构丨大数据丨机器学习丨运维


Kotlin与Java互操作