互操作就是在Kotlin中可以调用其他编程语言的接口,只要它们开放了接口,Kotlin就可以调用其成员属性和成员方法,这是其他编程语言所无法比拟的。同时,在进行Java编程时也可以调用Kotlin中的API接口。
Kotlin在设计时就考虑了与Java的互操作性。可以从Kotlin中自然地调用现有的Java代码,在Java代码中也可以很顺利地调用Kotlin代码。例如,在Kotlin中调用Java的Util的list库。
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
}
}
基本的互操作行为如下:
Kotlin可以自动识别Java中的getter/setter函数,而在Java中可以过getter/setter操作Kotlin属性。
import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // 调用 getFirstDayOfWeek()
calendar.firstDayOfWeek = Calendar.MONDAY // 调用ll setFirstDayOfWeek()
}
if (!calendar.isLenient) { // 调用 isLenient()
calendar.isLenient = true // 调用 setLenient()
}
}
循Java约定的getter和setter方法(名称以get开头的无参数方法和以set开头的单参数方法)在Kotlin中表示为属性。如果Java类只有一个setter,那么它在Kotlin中不会作为属性可见,因为Kotlin目前不支持只写(set-only)属性。
Kotlin的空安全类型的原理是,Kotlin在编译过程中会增加一个函数调用,对参数类型或者返回类型进行控制,开发者可以在开发时通过注解@Nullable和@NotNull方式来限制Java中空值异常。 Java中的任何引用都可能是null,这使得Kotlin对来自Java的对象进行严格的空安全检查是不现实的。Java声明的类型在Kotlin中称为平台类型,并会被特别对待。对这种类型的空检查要求会放宽,因此对它们的安全保证与在Java中相同。
val list = ArrayList<String>() // 非空(构造函数结果)
list.add("Item")
val size = list.size // 非空(原生 int)
val item = list[0] // 推断为平台类型(普通 Java 对象)
当调用平台类型变量的方法时,Kotlin不会在编译时报告可空性错误,但是在运行时调用可能会失败,因为空指针异常。
item.substring(1)//允许,如果item==null可能会抛出异常
平台类型是不可标识的,这意味着不能在代码中明确地标识它们。当把一个平台值赋给一个Kotlin变量时,可以依赖类型推断(该变量会具有所推断出的平台类型,如上例中item所具有的类型),或者选择我们所期望的类型(可空的或非空类型均可)。
val nullable:String?=item//允许,没有问题
Val notNull:String=item//允许,运行时可能失败
如果选择非空类型,编译器会在赋值时触发一个断言,这样可以防止Kotlin的非空变量保存空值。当把平台值传递给期待非空值等的Kotlin函数时,也会触发一个断言。总的来说,编译器尽力阻止空值的传播(由于泛型的原因,有时这不可能完全消除)。
如上所述,平台类型不能在程序中显式表述,因此在语言中没有相应语法。 然而,编译器和 IDE 有时需要(在错误信息中、参数信息中等)显示他们,Koltin提供助记符来表示他们:
由于泛型的原因,Kotlin在编译时可能出现空异常,而使用空注解可以有效的解决这一情况。编译器支持多种可空性注解:
在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),那么该注解类型自身将用于 检索精确的可空性,且具有与该可空性注解相同的含义。
@TypeQualifierNickname
@Nonnull(when = When.ALWAYS)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNonnull {
}
@TypeQualifierNickname
@CheckForNull // 另一个类型限定符别称的别称
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNullable {
}
interface A {
@MyNullable String foo(@MyNonnull String x);
// 在 Kotlin(严格模式)中:`fun foo(x: String): String?`
String bar(List<@MyNonnull String> x);
// 在 Kotlin(严格模式)中:`fun bar(x: List<String>!): String!`
}
@TypeQualifierDefault 引入应用时在所标注元素的作用域内定义默认可空性的注解。这些注解类型应自身同时标注有 @Nonnull(或其别称)与 @TypeQualifierDefault(…) 注解, 后者带有一到多个 ElementType 值。
当类型并未标注可空性注解时使用默认可空性,并且该默认值是由最内层标注有带有与所用类型相匹配的 ElementType 的类型限定符默认注解的元素确定。
@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}
@Nonnull(when = When.MAYBE)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE})
public @interface NullableApi {
}
@NullableApi
interface A {
String foo(String x); // fun foo(x: String?): String?
@NotNullApi // 覆盖来自接口的默认值
String bar(String x, @Nullable String y); // fun bar(x: String, y: String?): String
// 由于 `@NullableApi` 具有 `TYPE_USE` 元素类型,
// 因此认为 List<String> 类型参数是可空的:
String baz(List<String> x); // fun baz(List<String?>?): String?
// “x”参数仍然是平台类型,因为有显式
// UNKNOWN 标记的可空性注解:
String qux(@Nonnull(when = When.UNKNOWN) String x); // fun baz(x: String!): String?
}
也支持包级的默认可空性:
@NonNullApi // 默认将“test”包中所有类型声明为不可空
package test;
库的维护者可以使用 @UnderMigration 注解(在单独的构件 kotlin-annotations-jvm 中提供)来定义可为空性类型限定符的迁移状态。 @UnderMigration(status = …) 中的状态值指定了编译器如何处理 Kotlin 中注解类型的不当用法(例如,使用 @MyNullable 标注的类型值作为非空值):
库的维护者还可以将 @UnderMigration 状态添加到类型限定符别称与类型限定符默认值中。例如:
@Nonnull(when = When.ALWAYS)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
@UnderMigration(status = MigrationStatus.WARN)
public @interface NonNullApi {
}
// 类中的类型是非空的,但是只报警告
// 因为 `@NonNullApi` 标注了 `@UnderMigration(status = MigrationStatus.WARN)`
@NonNullApi
public class Test {}
注意:可空性注解的迁移状态并不会从其类型限定符别称继承,而是适用于默认类型限定符的用法。如果默认类型限定符使用类型限定符别称,并且它们都标注有 @UnderMigration,那么使用默认类型限定符的状态。
如果在Java中返回void,那么Kotlin返回的就是Unit。如果在调用时返回void,那么Kotlin会事先识别该返回值为void。
@JvmField是Kotlin和Java互相操作属性经常遇到的注解;@JvmStatic是将对象方法编译成Java静态方法;@JvmOverloads主要是Kotlin定义默认参数生成重载方法;@file:JvmName指定Kotlin文件编译之后生成的类名。
数据类本身属性没有默认的无参数的构造方法,因此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检查星投影的泛型类型。
if (a is List<Int>) // 错误:无法检查它是否真的是一个 Int 列表
// but
if (a is List<*>) // OK:不保证列表的内容
与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把一个 Array 赋值给一个 Array, 从而避免了可能的运行时故障。Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法, 但是对于 Java 方法,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。
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)
javaObj.removeIndices(array) // 将 int[] 传给方法
当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引入任何开销。
val array = arrayOf(1, 2, 3, 4)
array[x] = array[x] * 2 // 不会实际生成对 get() 和 set() 的调用
for (x in array) { // 不会创建迭代器
print(x)
}
即使当我们使用索引定位时,也不会引入任何开销:
for (i in array.indices) {// 不会创建迭代器
array[i] += 2
}
最后,in-检测也没有额外开销:
if (i in array.indices) { // 同 (i >= 0 && i < array.size)
print(array[i])
}
Java 类有时声明一个具有可变数量参数(varargs)的方法来使用索引。
public class JavaArrayExample {
public void removeIndicesVarArg(int... indices) {
// 函数体……
}
}
在这种情况下,你需要使用展开运算符 * 来传递 IntArray。
val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)
目前,无法传递 null 给一个声明为可变参数的方法。
就像Java 8一样,Kotlin支持SAM转换,这意味着Kotlin函数字面值可以被自动转换成只有一个非默认方法的Java接口的实现,只要这个方法的参数类型能够与这个Kotlin函数的参数类型相匹配就行。
首先使用Java创建一个SAMInJava类,然后通过Kotlin调用Java中的接口。
import java.util.ArrayList;
public class SAMInJava{
private ArrayList<Runnable>runnables=new ArrayList<Runnable>();
public void addTask(Runnable runnable){
runnables.add(runnable);
System.out.println("add:"+runnable+",size"+runnables.size());
}
Public void removeTask(Runnable runnable){
runnables.remove(runnable);
System.out.println("remove:"+runnable+"size"+runnables.size());
}
}
然后在Kotlin中调用该Java接口。
fun main(args: Array<String>) {
var samJava=SAMJava()
val lamba={
print("hello")
}
samJava.addTask(lamba)
samJava.removeTask(lamba)
}
运行结果为:
add:SAMKotlinKt$sam$Runnable$8b8e16f1@4617c264,size1
remove:SAMKotlinKt$sam$Runnable$8b8e16f1@36baf30csize1
如果Java类有多个接受函数式接口的方法,那么可以通过使用将Lambda表达式转换为特定的SAM类型的适配器函数来选择需要调用的方法。
val lamba={
print("hello")
}
samJava.addTask(lamba)
**注意:**SAM转换只适用于接口,而不适用于抽象类,即使这些抽象类只有一个抽象方法。此功能只适用于Java互操作;因为Kotlin具有合适的函数类型,所以不需要将函数自动转换为Kotlin接口的实现,因此不受支持。
除此之外,Kotlin调用Java还有很多的内容,读者可以通过下面的链接来了解:Kotlin调用Java
Java 可以轻松调用 Kotlin 代码。
Kotlin属性会被编译成以下Java元素:
例如,将Kotlin变量编译成Java中的变量声明。
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
如果属性名称是以is开头的,则使用不同的名称映射规则:getter的名称与属性名称相同,并且setter的名称是通过将is替换成set获得的。例如,对于属性isOpen,其getter会称作isOpen(),而其setter会称作setOpen()。这一规则适用于任何类型的属性,并不仅限于Boolean。
例如,在org.foo.bar 包内的 example.kt 文件中声明的所有的函数和属性,包括扩展函数, 该 类会编译成一个名为 org.foo.bar.ExampleKt 的 Java 类的静态方法。 首先,新建一个ExampleKt.kt的文件,并新建一个bar函数:
package demo
class Foo
fun bar(){
println("这只是一个bar方法")
}
然后,在Java中调用这个函数。
package demo;
public class Example {
public static void main(String[]args){
demo.ExampleKtKt.bar();
}
}
当然,可以使用@JvmName注解修改所生成的Java类的类名。例如:
@file:JvmName("Demo")
package demo
那么在Java调用时就需要修改类名。例如:
public class Example {
public static void main(String[]args){
demo.Demo.bar();
}
}
在多个文件中生成相同的Java类名(包名相同并且类名相同或者有相同的@JvmName注解)通常是错误的。然而,编译器能够生成一个单一的Java外观类,它具有指定的名称且包含来自于所有文件中具有该名称的所有声明。要生成这样的外观,请在所有的相关文件中使用@JvmMultifileClass注解。
@file:JvmName("example")
@file:JvmMultifileClass
package demo
如果需要在Java中将Kotlin属性作为字段暴露,那么就需要使用@JvmField注解对其进行标注。使用@JvmField注解标注后,该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(Backing Field)、非私有的、没有open/override或者const修饰符,并且不是被委托的属性,那么可以使用@JvmField注解该属性。
首先,新建一个kt类,并添加如下代码。
class C(id: String) {
@JvmField val ID = id
}
然后在Java中调用该代码,
class JavaClient {
public String getID(C c) {
return c.ID;
}
}
延迟初始化的属性(在Java中)也会暴露为字段, 该字段的可见性与 lateinit 属性的 setter 相同。
在命名对象或伴生对象时,声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中包含静态幕后字段。通常这些字段是私有的,但可以通过以下方式之一暴露出来。
使用 @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中使用该字段的属性。
// Java
Singleton.provider = new Provider();
// 在 Singleton 类中的 public static 非-final 字段
用 const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段,首先新建一个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将包级函数表示为静态方法。如果对这些函数使用@JvmStatic进行标注,那么Kotlin还可以为在命名对象或伴生对象中定义的函数生成静态方法。如果使用该注解,那么编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。例如:
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 方法在该对象或包含该伴生对象的类中是静态成员。
Kotlin的可见性以下列方式映射到Java代码中。
有时你需要调用有 KClass 类型参数的 Kotlin 方法。 因为没有从 Class 到 KClass 的自动转换,所以你必须通过调用 Class.kotlin 扩展属性的等价形式来手动进行转换。例如:
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)
有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称,最突出的例子是由于类型擦除引发的。
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
这两个函数不能同时定义在一个类中,因为它们的 JVM 签名是一样的。如果我们真的希望它们在 Kotlin 中使用相同的名称,可以使用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数。例如:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。同样的技巧也适用于属性中。例如:
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
通常,如果你写一个有默认参数值的 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 注解也有效。
如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常, 于是如果我们有一个这样的 Kotlin 函数。首先,新建一个kt文件。
example.kt
package demo
fun foo() {
throw IOException()
}
然后,在 Java 中调用它的时候,需要使用try{}catch{}来捕捉这个异常。
// Java
try {
demo.Example.foo();
}
catch (IOException e) { // 错误:foo() 未在 throws 列表中声明 IOException
// ……
}
因为 foo() 没有声明 IOException,我们从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws 注解。
@Throws(IOException::class)
fun foo() {
throw IOException()
}
当从Java中调用Kotlin函数时,没有任何方法可以阻止Kotlin中的空值传入。Kotlin在JVM虚拟机中运行时会检查所有的公共函数,可以检查非空值,这时候就可以通过NullPointerException得到Java中的非空值代码。
当 Kotlin 的类使用了声明处型变时,可以通过两种方式从Java代码中看到它们的用法。让我们假设我们有以下类和两个使用它的函数:
class Box<out T>(val value: T)
interface Base
class Derived : Base
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
将这两个函数转换成Java代码如下:
Box<Derived> boxDerived(Derived value) { …… }
Base unboxBase(Box<Base> box) { …… }
问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived(“s”)),但是在 Java 中是行不通的,因为在 Java 中类 Box 在其泛型参数 T 上是不型变的,于是 Box 并不是 Box 的子类。 要使其在 Java 中工作,我们按以下这样定义 unboxBase。
Base unboxBase(Box<? extends Base> box) { …… }
这里我们使用 Java 的通配符类型(? extends Base)来通过使用处型变来模拟声明处型变,因为在 Java 中只能这样。
当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中工作,对于协变定义的 Box 我们生成 Box 作为 Box
// 作为返回类型——没有通配符
Box<Derived> boxDerived(Derived value) { …… }
// 作为参数——有通配符
Base unboxBase(Box<? extends Base> box) { …… }
注意:当参数类型是 final 时,生成通配符通常没有意义,所以无论在什么地方 Box 始终转换为 Box。如果我们在默认不生成通配符的地方需要通配符,我们可以使用 @JvmWildcard 注解:
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// 将被转换成
// Box<? extends Derived> boxDerived(Derived value) { …… }
另一方面,如果我们根本不需要默认的通配符转换,我们可以使用@JvmSuppressWildcards。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// 会翻译成
// Base unboxBase(Box<Base> box) { …… }
注意:@JvmSuppressWildcards 不仅可用于单个类型参数,还可用于整个声明(如函数或类),从而抑制其中的所有通配符。
类型 Nothing 是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行,以为这种类型不能在 Java 中被准确表示。这就是为什么在使用 Nothing 参数的地方 Kotlin 生成一个原始类型:
fun emptyList(): List<Nothing> = listOf()
// 会翻译成
// List emptyList() { …… }