7.1.2 从 Kotlin 调用 Java
17 分钟阅读
7.1.2 从 Kotlin 调用 Java
Kotlin 在设计时就考虑了 Java 互操作。现有的 Java 代码可以从 Kotlin 以自然的方式调用,Kotlin 代码也可以相当顺畅地从 Java 中使用。本节介绍从 Kotlin 调用 Java 代码的一些细节。
几乎所有的 Java 代码都可以毫无问题地使用:
| |
Getter 与 setter
遵循 Java getter 和 setter 约定的方法(以 get 开头的无参方法和以 set 开头的单参数方法)在 Kotlin 中表示为属性。这类属性也称为合成属性。Boolean 访问器方法(getter 名以 is 开头、setter 名以 set 开头)会表示为与 getter 方法同名的属性。
| |
上面的 calendar.firstDayOfWeek 就是合成属性的一个例子。
注意,如果 Java 类只有 setter,它在 Kotlin 中不会作为属性可见,因为 Kotlin 不支持只有 setter 的属性。
Java 合成属性引用
警告: 此特性是实验性的。它随时可能被移除或更改。我们建议你只把它用于评估目的。
从 Kotlin 1.8.20 开始,你可以创建对 Java 合成属性的引用。考虑下面的 Java 代码:
| |
Kotlin 一直允许你写 person.age,其中 age 就是合成属性。现在你还可以创建对 Person::age 和 person::age 的引用。name 也是如此。
| |
如何启用 Java 合成属性引用
要启用该特性,请设置 -language-version 2.1 编译器选项。在 Gradle 项目中,你可以在 build.gradle(.kts) 中添加以下内容:
Kotlin
| |
Groovy
| |
注意: 在 Kotlin 1.9.0 之前,要启用该特性必须设置
-language-version 1.9编译器选项。
返回 void 的方法
如果 Java 方法返回 void,从 Kotlin 调用时它会返回 Unit。如果有人使用了该返回值,Kotlin 编译器会在调用处为它赋值,因为这个值本身是已知的(就是 Unit)。
为 Kotlin 关键字形式的 Java 标识符转义
有些 Kotlin 关键字在 Java 中是合法的标识符:in、object、is 等等。如果某个 Java 库把 Kotlin 关键字用作方法名,你仍然可以通过反引号(`)字符转义来调用该方法:
| |
空安全与平台类型
Java 中的任何引用都可能是 null,这让 Kotlin 严格的空安全要求在来自 Java 的对象上难以完全适用。Java 声明的类型在 Kotlin 中被视为无法显式书写的类型,称为平台类型。你无法在代码中显式写出这些不可书写类型。因此,当把平台值赋给 Kotlin 变量时,你可以:
- 依赖类型推断。此时变量具有推断出的平台类型。
- 选择你期望的类型。Kotlin 允许可空类型和非可空类型。
对这类类型会放宽空检查,因此它们的安全保证与 Java 相同(更多内容见下文)。请看以下例子:
| |
在平台类型的变量上调用方法时,Kotlin 不会在编译期报可空性错误,但调用可能在运行时失败,原因可能是空指针异常,也可能是 Kotlin 为防止 null 传播而生成的断言:
| |
你可以把平台类型的值同时赋给 Kotlin 的可空类型和非可空类型变量。不过,如果你把这样的值赋给非可空类型变量,而该值在运行时实际为 null,Kotlin 会抛出 NullPointerException。要避免这种情况,请为你的 Kotlin 代码添加显式的可空性:
| |
如果你选择非可空类型,编译器会在赋值时插入断言。这可以防止 Kotlin 的非可空变量持有 null。当你把平台值传给期望非空值的 Kotlin 函数时,以及其他情况下,也会插入断言。总体而言,编译器会尽力防止 null 在程序中大范围传播,不过由于泛型的存在,有时无法完全消除。
平台类型的记法
如上一节所述,平台类型无法在程序中显式提及,因此语言中没有它们的语法。不过编译器和 IDE 有时需要显示它们(例如在错误消息或参数信息中),因此它们有一套助记记法:
T!表示"T或T?"(Mutable)Collection<T>!表示"Java 的T集合,可变或不可变、可空或不可空皆可"Array<(out) T>!表示"Java 的T(或T的子类型)数组,可空或不可空皆可"
当你在错误消息或 IDE 提示中看到这种记法时,请为你的 Kotlin 变量添加显式类型标注以恢复空安全检查,或用可空性注解在源头消除平台类型。
可空性注解
带可空性注解的 Java 类型不会被表示为平台类型,而是表示为真正的可空或非可空 Kotlin 类型。编译器支持多种可空性注解,包括:
- JetBrains(
org.jetbrains.annotations包中的@Nullable和@NotNull) - JSpecify(
org.jspecify.annotations) - 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) - RxJava 3(
io.reactivex.rxjava3.annotations) - Vert.x(
io.vertx.codegen.annotations)
你可以用以下编译器选项指示编译器针对特定的可空性注解报告可空性不匹配:
| |
请为完全限定的可空性注解指定包名,并从以下报告级别中选择一个:
ignore忽略可空性不匹配warn报告警告strict报告错误。
注意: JSpecify 是唯一默认使用
strict报告级别的受支持风格。使用它可以在没有额外配置的情况下对可空性注解报告错误。
受支持的可空性注解完整列表见 Kotlin 编译器源码。
可变性注解
你可以用可变性注解标注 Java 声明,以指定其返回的集合在 Kotlin 中是只读还是可变的。如果你把该值赋给可变性不同的集合类型,编译器会报告类型不匹配。诊断的严重程度取决于具体的可变性注解。
编译器支持多种可变性注解,包括:
kotlin.annotations.jvm.ReadOnlykotlin.annotations.jvm.Mutableorg.jetbrains.annotations.Unmodifiableorg.jetbrains.annotations.UnmodifiableView
受支持的可变性注解完整列表见 Kotlin 编译器源码。
为类型实参与类型参数添加注解
你可以为泛型类型的类型实参和类型参数添加注解,从而也为它们提供可空性信息。
注意: 本节所有例子都使用来自
org.jetbrains.annotations包的 JetBrains 可空性注解。
类型实参
考虑 Java 声明上的这些注解:
| |
它们在 Kotlin 中会产生如下签名:
| |
当类型实参上缺少 @NotNull 注解时,你会得到平台类型:
| |
Kotlin 也会考虑基类和接口的类型实参上的可空性注解。例如,下面两个 Java 类的签名如下:
| |
| |
在 Kotlin 代码中,把 Derived 的实例传给假定期望 Base<String> 的地方会产生警告。
| |
Derived 的上界被设为 Base<String?>,它与 Base<String> 不同。
进一步了解 Kotlin 中的 Java 泛型。
类型参数
默认情况下,Kotlin 和 Java 中普通类型参数的可空性都是未定义的。在 Java 中,你可以用可空性注解来指定它。让我们给 Base 类的类型参数加上注解:
| |
在继承 Base 时,Kotlin 期望非空的类型实参或类型参数。因此,下面的 Kotlin 代码会产生警告:
| |
你可以通过指定上界 K : Any 来修正它。
Kotlin 也支持在 Java 类型参数的边界上使用可空性注解。让我们给 Base 加上边界:
| |
Kotlin 会这样翻译它:
| |
因此,把可空类型作为类型实参或类型参数传入会产生警告。
为类型实参和类型参数添加注解适用于 Java 8 或更高的目标。该特性要求可空性注解支持 TYPE_USE 目标(org.jetbrains.annotations 从 15 版起支持)。
注意: 如果某个可空性注解除了
TYPE_USE之外还支持其他适用于类型的目标,TYPE_USE优先。例如,如果@Nullable同时有TYPE_USE和METHOD目标,Java 方法签名@Nullable String[] f()在 Kotlin 中会变成fun f(): Array<String?>!。
JSpecify 支持
Kotlin 支持 JSpecify 可空性注解,它为 Java 可空性提供了一组统一的注解。JSpecify 让你可以为 Java 声明提供详细的可空性信息,帮助 Kotlin 在与 Java 代码交互时保持空安全。
Kotlin 支持 org.jspecify.annotations 包中的以下注解:
@Nullable把类型标记为可空。@NonNull把类型标记为非空。@NullMarked把某个作用域(例如一个类或包)内的所有类型默认标记为非空,除非另有注解。
该注解不适用于局部变量和类型变量(泛型)。在提供具体的可空或非空类型之前,类型变量保持"空不可知"状态。
@NullUnmarked逆转@NullMarked的效果,把作用域内的所有类型变为平台类型。
考虑下面这个带 JSpecify 注解的 Java 类:
| |
在 Kotlin 中,它们被视为普通的可空和非空类型,而不是平台类型:
| |
默认情况下,Kotlin 编译器会把 JSpecify 注解的可空性不匹配报告为错误。你可以用以下编译器选项自定义 JSpecify 可空性诊断的严重程度:
| |
可用的报告级别有:
| 级别 | 说明 |
| strict | 对可空性不匹配报告错误(默认)。 |
| warn | 报告警告。 |
| ignore | 忽略可空性不匹配。 |
关于 JSpecify 注解的更多信息,请参阅 JSpecify 用户指南。
JSR-305 支持
JSR-305 中定义的 @Nonnull 注解受支持,用于表示 Java 类型的可空性。
如果 @Nonnull(when = ...) 的值是 When.ALWAYS,被注解的类型被视为非空;When.MAYBE 和 When.NEVER 表示可空类型;而 When.UNKNOWN 则强制该类型成为平台类型。
库可以基于 JSR-305 注解进行编译,但不必把注解释出的构件(例如 jsr305.jar)作为库使用者的编译依赖。Kotlin 编译器可以在 classpath 上没有这些注解的情况下从库中读取 JSR-305 注解。
自定义可空性限定符(KEEP-79)也受支持(见下文)。
类型限定符昵称
如果某个注解类型同时被 @TypeQualifierNickname 和 JSR-305 的 @Nonnull(或它的另一个昵称,例如 @CheckForNull)标注,那么该注解类型本身就会被用于获取精确的可空性,并且与那个可空性注解含义相同:
| |
类型限定符默认值
@TypeQualifierDefault 允许引入这样的注解:当它被应用时,定义被注解元素作用域内的默认可空性。
这样的注解类型本身应当同时被 @Nonnull(或其昵称)和 @TypeQualifierDefault(...) 标注,并带有一个或多个 ElementType 值:
ElementType.METHOD用于方法的返回类型ElementType.PARAMETER用于值参数ElementType.FIELD用于字段ElementType.TYPE_USE用于任何类型,包括类型实参、类型参数的上界和通配符类型
当类型本身没有被可空性注解标注时,就使用默认可空性;该默认值由最内层的、带类型限定符默认注解且其 ElementType 与类型用法相匹配的外层元素决定。
| |
注意: 这个例子中的类型只有在启用 strict 模式时才会生效;否则仍是平台类型。请参阅
@UnderMigration注解和编译器配置两节。
包级默认可空性也受支持。
| |
@UnderMigration 注解
@UnderMigration 注解(在单独的构件 kotlin-annotations-jvm 中提供)可以被库作者用来定义可空性类型限定符的迁移状态。
@UnderMigration(status = ...) 中的状态值决定编译器如何处理该库中被注解类型的不当用法:
MigrationStatus.STRICT让注解像普通的可空性注解一样工作,即对不当用法报告错误,并影响被注解声明主体中的类型推断。MigrationStatus.WARN:不当用法被报告为编译警告,而不是错误,但被注解声明主体中的类型仍然被视为平台类型。MigrationStatus.IGNORE让编译器完全忽略该可空性注解。
库维护者可以把 @UnderMigration 状态同时添加到类型限定符昵称和类型限定符默认值中。
| |
注意: 可空性类型限定符的迁移状态不会被
@UnderMigration注解的值覆盖,也不会被-Xjsr305=under-migration:...覆盖。
如果一个类型限定符默认值使用了某个类型限定符昵称,那么 @UnderMigration 的迁移状态就取自该昵称。
编译器配置
JSR-305 检查可以通过向编译器添加以下选项来配置:
-Xjsr305={strict|warn|ignore}设置@Nonnull等非@UnderMigration注解的行为。-Xjsr305=under-migration:{strict|warn|ignore}覆盖@UnderMigration注解的行为。-Xjsr305=@<fq.name>:{strict|warn|ignore}覆盖特定注解的行为,其中<fq.name>是该注解的完全限定名。
strict、warn 和 ignore 值的含义与 JSpecify 中对应级别的含义相同。
注意: 提示:内置的 JSR-305 注解
@Nonnull、@Nullable和@CheckForNull始终启用,无论-Xjsr305标志如何配置编译器,它们都会影响 Kotlin 中被注解声明的类型。
例如,添加 -Xjsr305=ignore -Xjsr305=under-migration:ignore 会让编译器忽略所有提到的注解,包括来自 @UnderMigration 的注解和来自 kotlin-annotations-jvm 的注解。
默认行为与 -Xjsr305=warn 相同。
映射的类型
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.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 类型 | Kotlin 类型 |
| 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? |
注意,用作类型参数的装箱基本类型会被映射为平台类型:例如 List<java.lang.Integer> 在 Kotlin 中会变成 List<Int!>。
集合类型在 Kotlin 中可以是只读的或可变的,因此 Java 的集合如下映射(本表中的所有 Kotlin 类型都位于 kotlin.collections 包中):
| Java 类型 | Kotlin 只读类型 | Kotlin 可变类型 | 加载后的平台类型 |
| Iterator<T> | Iterator<T> | MutableIterator<T> | (Mutable)Iterator<T>! |
| Iterable<T> | Iterable<T> | MutableIterable<T> | (Mutable)Iterable<T>! |
| Collection<T> | Collection<T> | MutableCollection<T> | (Mutable)Collection<T>! |
| Set<T> | Set<T> | MutableSet<T> | (Mutable)Set<T>! |
| List<T> | List<T> | MutableList<T> | (Mutable)List<T>! |
| ListIterator<T> | ListIterator<T> | MutableListIterator<T> | (Mutable)ListIterator<T>! |
| Map<K, V> | Map<K, V> | MutableMap<K, V> | (Mutable)Map<K, V>! |
| Map.Entry<K, V> | Map.Entry<K, V> | MutableMap.MutableEntry<K,V> | (Mutable)Map.(Mutable)Entry<K, V>! |
Java 的数组按下文所述方式映射:
| Java 类型 | Kotlin 类型 |
| int[] | kotlin.IntArray! |
| String[] | kotlin.Array<(out) String!>! |
这些 Java 类型的静态成员不能直接在对应 Kotlin 类型的伴生对象上访问。要调用它们,请使用 Java 类型的完全限定名,例如
java.lang.Integer.toHexString(foo)。
Kotlin 中的 Java 泛型
Kotlin 的泛型与 Java 略有不同(参见泛型)。把 Java 类型导入 Kotlin 时,会进行以下转换:
Java 的通配符被转换为类型投影:
Foo<? extends Bar>变成Foo<out Bar!>!Foo<? super Bar>变成Foo<in Bar!>!Java 的原始类型被转换为星投影:
List变成List<*>!,也就是List<out Any?>!
与 Java 一样,Kotlin 的泛型在运行时也不会保留:对象不携带传给其构造函数的实际类型实参信息。例如 ArrayList<Integer>() 与 ArrayList<Character>() 无法区分。这使得无法进行考虑泛型的 is 检查。Kotlin 只允许对星投影的泛型类型做 is 检查:
| |
Java 数组
Kotlin 中的数组是不变的,与 Java 不同。这意味着 Kotlin 不允许你把 Array<String> 赋给 Array<Any>,从而避免了可能的运行时失败。把子类数组作为超类数组传给 Kotlin 方法也是被禁止的,但对 Java 方法来说,可以通过形如 Array<(out) String>! 的平台类型做到这一点。
在 Java 平台上,数组与基本数据类型一起使用,以避免装箱/拆箱操作的开销。由于 Kotlin 隐藏了这些实现细节,与 Java 代码对接时就需要一种变通办法。为此,每种基本类型数组都有专门的类(IntArray、DoubleArray、CharArray 等)。它们与 Array 类无关,并且为了获得最佳性能会被编译为 Java 的基本类型数组。
假设有一个接受 int 索引数组的 Java 方法:
| |
要传递基本类型的数组,你可以在 Kotlin 中这样做:
| |
编译为 JVM 字节码时,编译器会优化对数组的访问,因此不会引入额外开销:
| |
即使你使用索引进行访问,也不会引入任何开销:
| |
最后,in 检查也没有任何开销:
| |
Java 可变参数
Java 类有时会用可变数量参数(varargs)来声明索引方法:
| |
在这种情况下,你需要使用展开运算符 * 来传递 IntArray:
| |
运算符
由于 Java 无法标记哪些方法适合使用运算符语法,Kotlin 允许把任何名称和签名合适的 Java 方法用作运算符重载和其他约定(例如 invoke())。不允许用中缀调用语法调用 Java 方法。
受检异常
在 Kotlin 中,所有异常都是非受检的,也就是说编译器不会强制你捕获任何异常。因此当你调用声明了受检异常的 Java 方法时,Kotlin 不会强制你做任何事:
| |
对象方法
把 Java 类型导入 Kotlin 时,java.lang.Object 类型的所有引用都会变成 Any。由于 Any 不是平台特定的,它只把 toString()、hashCode() 和 equals() 声明为成员,因此为了让 java.lang.Object 的其他成员可用,Kotlin 使用了扩展函数。
wait() 与 notify()
方法 wait() 和 notify() 在 Any 类型的引用上不可用。通常不鼓励使用它们,建议改用 java.util.concurrent。
如果你确实需要调用这些方法,请通过 Java 对象访问它们,并抑制 PLATFORM_CLASS_MAPPED_TO_KOTLIN 警告:
| |
或者显式转换为 java.lang.Object 并抑制 PLATFORM_CLASS_MAPPED_TO_KOTLIN 警告:
| |
getClass()
要获取对象的 Java 类,请使用类引用上的 java 扩展属性:
| |
上面的代码使用了绑定的类引用。你也可以使用 javaClass 扩展属性:
| |
clone()
要重写 clone(),你的类需要继承 kotlin.Cloneable:
| |
不要忘了 Effective Java, 3rd Edition 的第 13 条:谨慎地重写 clone。
finalize()
要重写 finalize(),你只需声明它即可,不需要使用 override 关键字:
| |
按照 Java 的规则,finalize() 不能是 private。
从 Java 类继承
在 Kotlin 中,一个类最多只能有一个 Java 类作为超类型(Java 接口的数量不限)。
访问静态成员
Java 类的静态成员构成这些类的"伴生对象"。你不能把这样的"伴生对象"当作值传递,但可以显式访问其成员,例如:
| |
要访问映射到 Kotlin 类型的 Java 类型的静态成员,请使用该 Java 类型的完全限定名:java.lang.Integer.bitCount(foo)。
Java 反射
Java 反射可用于 Kotlin 类,反之亦然。如上所述,你可以使用 instance::class.java、ClassName::class.java 或 instance.javaClass 通过 java.lang.Class 进入 Java 反射。不要为此使用 ClassName.javaClass,因为它指向 ClassName 的伴生对象类,等同于 ClassName.Companion::class.java,而不是 ClassName::class.java。
对于每种基本类型,都有两个不同的 Java 类,Kotlin 提供了获取两者的方式。例如 Int::class.java 会返回表示基本类型本身的类实例,对应 Java 中的 Integer.TYPE。要获取相应包装类型的类,请使用 Int::class.javaObjectType,它等价于 Java 的 Integer.class。
其他受支持的情况包括:为 Kotlin 属性获取 Java getter/setter 方法或幕后字段,为 Java 字段获取 KProperty,为 KFunction 获取 Java 方法或构造函数,反之亦然。
SAM 转换
Kotlin 同时支持 Java 和 Kotlin 接口的 SAM 转换。对 Java 的支持意味着,只要 Java 接口方法的参数类型与 Kotlin 函数的参数类型匹配,Kotlin 函数字面量就能自动转换为具有单个非默认方法的 Java 接口的实现。
你可以用它来创建 SAM 接口的实例:
| |
……以及在方法调用中使用:
| |
如果 Java 类有多个接受函数式接口的方法,你可以使用一个把 lambda 转换为特定 SAM 类型的适配函数来选择需要调用的那个。这些适配函数也会在需要时由编译器生成:
| |
SAM 转换只适用于接口,不适用于抽象类,即使抽象类也只有一个 抽象方法。
在 Kotlin 中使用 JNI
要声明一个用原生(C 或 C++)代码实现的函数,你需要用 external 修饰符标记它:
| |
其余步骤与 Java 中的做法完全相同。
你也可以把属性 getter 和 setter 标记为 external:
| |
在底层,这会创建两个函数 getMyProperty 和 setMyProperty,两者都标记为 external。
在 Kotlin 中使用 Lombok 生成的声明
你可以在 Kotlin 代码中使用 Java 的 Lombok 生成的声明。如果你需要在同一个 Java/Kotlin 混合模块中生成并使用这些声明,可以在 Lombok 编译器插件页面了解如何操作。如果你是从另一个模块调用这些声明,则在编译那个模块时不需要使用该插件。