在 Kotlin 中调用 Java
Kotlin 是以与 Java 的互操作性为设计目标的。现有的 Java 代码可以自然地从 Kotlin 中调用,Kotlin 代码也可以相对流畅地用于 Java。 在本节中,我们将描述从 Kotlin 调用 Java 代码的一些细节。
几乎所有的 Java 代码都可以毫无问题地使用:
Getter 和 setter
遵循 Java getter 和 setter 约定的方法(无参数方法且以 get
开头的名称,以及单参数方法且以 set
开头的名称)在 Kotlin 中被表示为属性。 此类属性也称为 合成属性。 Boolean
的访问器方法(getter 方法的名称以 is
开头,setter 方法的名称以 set
开头) 在 Kotlin 中表示为与 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)
来实现:
返回 void 的方法
如果一个 Java 方法返回 void
,则在 Kotlin 中调用时会返回 Unit
。
如果有人不小心使用了该返回值,Kotlin 编译器将在调用点进行赋值,因为该值在预先是已知的(为 Unit
)。
对于在 Kotlin 中为关键字的 Java 标识符进行转义
一些 Kotlin 关键字在 Java 中是有效的标识符: in
、 object
、 is
等。
如果一个 Java 库使用 Kotlin 关键字作为方法名,你仍然可以通过使用反引号(`)字符来调用该方法:
空安全和平台类型
在 Java 中,任何引用都可能为 null
,这使得 Kotlin 对来自 Java 的对象严格的空安全要求变得不切实际。
Java 声明的类型在 Kotlin 中以特定方式处理,称为 平台类型。 对于此类类型的空检查被放宽,因此它们的安全性保证与 Java 相同(详见 下面)。
考虑以下示例:
当你在平台类型的变量上调用方法时,Kotlin 不会在编译时发出空值错误, 但该调用可能在运行时失败,因为会发生空指针异常或 Kotlin 生成的断言以防止空值传播:
平台类型是 不可表述的 ,这意味着你不能在语言中显式地书写它们。 当一个平台值被赋值给 Kotlin 变量时,你可以依赖类型推断(该变量将具有推断的平台类型,正如上面示例中的 item
),或者你可以选择你期望的类型(允许 nullable 和 non-nullable 类型):
如果你选择了非空类型,编译器将在赋值时发出断言。这防止了 Kotlin 的非空变量持有 null。 当你将平台值传递给期望非空值的 Kotlin 函数时,编译器也会发出断言,其他情况亦然。 总体而言,编译器尽力防止 null 在程序中传播,尽管有时由于泛型,这种传播是无法完全消除的。
平台类型的表示法
如上所述,平台类型不能在程序中显式提及,因此语言中没有它们的语法。
然而,编译器和 IDE 有时需要显示这些类型(例如,在错误消息或参数信息中),因此有一种助记符表示法:
T!
表示 "T
或T?
",(Mutable)Collection<T>!
表示 "Java 集合的T
可能是可变的,也可能不是,可能是可空的,也可能不是",Array<(out) T>!
表示 "Java 数组的T
(或T
的子类型),可能是可空的,也可能不是"。
空值注解
具有空值注解的 Java 类型被表示为实际的可空或非空 Kotlin 类型,而不是平台类型。编译器支持几种类型的空值注解,包括:
JetBrains (来自
org.jetbrains.annotations
包的@Nullable
和@NotNull
)JSpecify (
org.jspecify.nullness
)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
)
你可以设置编译器,是否根据特定类型的空值注解来报告空值不匹配问题。 使用编译器选项 -Xnullability-annotations=@<package-name>:<report-level>
。 在参数中,指定完全限定的空值注解包和以下报告级别之一:
ignore
以忽略空值不匹配warn
以报告警告strict
以报告错误。
请参阅Kotlin 编译器源代码中支持的空值注解的完整列表。
注解类型实参和类型形参
你可以注解泛型类型的类型实参和类型形参,以提供它们的空值信息。
类型实参
考虑以下 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 及更高版本中支持此目标)。 使用 -Xtype-enhancement-improvements-strict-mode
编译器选项可以报告使用与 Java 的空值注解不符的 Kotlin 代码中的错误。
JSR-305 支持
@Nonnull
注解在 JSR-305 中定义,用于表示 Java 类型的可空性。
如果 @Nonnull(when = ...)
的值为 When.ALWAYS
,则标注的类型被视为非空类型; When.MAYBE
和 When.NEVER
表示可空类型;而 When.UNKNOWN
强制类型为 平台类型。
一个库可以针对 JSR-305 注解进行编译,但不需要将注解工件(例如 jsr305.jar
) 作为库消费者的编译依赖。Kotlin 编译器可以从没有注解存在于类路径上的库中读取 JSR-305 注解。
自定义可空性限定符 (KEEP-79) 也得到了支持(见下文)。
类型限定符别名
如果一个注解类型同时被标注为 @TypeQualifierNickname
和 JSR-305 的 @Nonnull
(或其另一个别名,例如 @CheckForNull
),则该注解类型本身用于获取精确的可空性, 并且具有与该可空性注解相同的含义:
类型限定符默认值
@TypeQualifierDefault
允许引入注解,当应用时,在被注解元素的范围内定义默认的可空性。
这样的注解类型应该同时被标注为 @Nonnull
(或其别名)和 @TypeQualifierDefault(...)
,并且具有一个或多个 ElementType
值:
ElementType.METHOD
用于方法的返回类型ElementType.PARAMETER
用于值参数ElementType.FIELD
用于字段ElementType.TYPE_USE
用于任何类型,包括类型参数的类型实参、上界和通配符类型
当类型本身没有被可空性注解标注时,会使用默认的可空性,默认值由最近的被标注为类型限定符默认注解的元素确定,该元素的 ElementType
与类型使用相匹配。
包级默认可空性也得到了支持:
@UnderMigration 注解
@UnderMigration
注解(提供在单独的工件 kotlin-annotations-jvm
中)可以被库维护者用来定义可空性类型限定符的迁移状态。
@UnderMigration(status = ...)
中的状态值指定了编译器如何处理 Kotlin 中对标注类型的不当用法(例如,将标注为 @MyNullable
的类型值用作非空):
MigrationStatus.STRICT
使注解像任何普通可空性注解一样工作,即对不当用法报告错误,并影响标注声明中类型在 Kotlin 中的表现。MigrationStatus.WARN
:不当用法会被报告为编译警告而不是错误,但标注声明中的类型保持为平台类型。MigrationStatus.IGNORE
使编译器完全忽略可空性注解。
库维护者可以将 @UnderMigration
状态添加到类型限定符别名和类型限定符默认值中:
如果默认类型限定符使用了类型限定符别名,并且它们都标注为 @UnderMigration
,则使用默认类型限定符中的状态。
编译器配置
可以通过添加 -Xjsr305
编译器标志和以下选项(及其组合)来配置 JSR-305 检查:
-Xjsr305={strict|warn|ignore}
设置非@UnderMigration
注解的行为。
自定义可空性限定符,尤其是@TypeQualifierDefault
,已广泛应用于许多知名库中,因此用户在更新到支持 JSR-305 的 Kotlin 版本时可能需要平稳迁移。自 Kotlin 1.1.60 以来,此标志仅影响非@UnderMigration
注解。-Xjsr305=under-migration:{strict|warn|ignore}
用于覆盖@UnderMigration
注解的行为。
用户可能对库的迁移状态有不同看法:他们可能希望在官方迁移状态为WARN
时强制显示错误,或反之,他们希望推迟错误报告,直到完成迁移。-Xjsr305=@<fq.name>:{strict|warn|ignore}
用于覆盖单个注解的行为,其中<fq.name>
是注解的完全限定类名。 可以针对不同的注解多次出现。此选项在管理特定库的迁移状态时非常有用。
strict
、 warn
和 ignore
值的含义与 MigrationStatus
的含义相同,并且只有 strict
模式会影响 Kotlin 中查看到的注解声明中的类型。
例如,添加 -Xjsr305=ignore -Xjsr305=under-migration:ignore -Xjsr305=@org.library.MyNullable:warn
到编译器参数中,会使编译器为不当使用 @org.library.MyNullable
注解的类型生成警告,并忽略所有其他 JSR-305 注解。
默认行为与 -Xjsr305=warn
相同。 strict
值应视为实验性功能(将来可能会添加更多检查)。
映射类型
Kotlin 特别处理某些 Java 类型。这些类型不会直接从 Java 加载,而是会 映射 到相应的 Kotlin 类型。 此映射仅在编译时生效,运行时表示保持不变。 Java 的基本类型会映射到对应的 Kotlin 类型(需考虑平台类型):
Java 类型 | Kotlin 类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
一些非原始内置类也被映射:
Java 类型 | Kotlin 类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java 的装箱基本类型被映射为可空的 Kotlin 类型:
Java 类型 | Kotlin 类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
请注意,作为类型形参使用的装箱基本类型会映射为平台类型: 例如, List<java.lang.Integer>
在 Kotlin 中会变成 List<Int!>
。
在 Kotlin 中,集合类型可以是只读的或可变的,因此 Java 的集合映射如下表所示 (本表中所有 Kotlin 类型都位于 kotlin.collections
包中):
Java 类型 | Kotlin 只读类型 | Kotlin 可变类型 | 加载的平台类型 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java 的数组映射如下方所述:
Java 类型 | Kotlin 类型 |
---|---|
|
|
|
|
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 数组
与 Java 不同,Kotlin 中的数组是不可变的。 这意味着 Kotlin 不允许将 Array<String>
赋值给 Array<Any>
,从而防止可能的运行时失败。 将子类数组作为超类数组传递给 Kotlin 方法也是禁止的,但对于 Java 方法,通过 Array<(out) String>!
形式的平台类型可以实现此操作。
在 Java 平台上,数组与基本数据类型一起使用,以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个解决方法来与 Java 代码交互。 对于每种基本数据类型,都有专门的数组类(如 IntArray
、 DoubleArray
、 CharArray
等)来处理这种情况。 这些类与 Array
类无关,并且会编译为 Java 的基本类型数组以获得最大性能。
假设有一个 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.lang.Object
:
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 的支持意味着,Kotlin 函数字面量可以自动转换为实现 Java 接口的单一非默认方法,只要接口方法的参数类型与 Kotlin 函数的参数类型匹配。
你可以用它来创建 SAM 接口的实例:
...并在方法调用中使用:
如果 Java 类有多个接受函数接口的方法,你可以通过使用适配器函数将 lambda 转换为特定的 SAM 类型来选择你需要调用的方法。这些适配器函数在需要时也会由编译器生成:
在 Kotlin 中使用 JNI
要声明一个在原生(C 或 C++)代码中实现的函数,你需要用 external
修饰符标记它:
其余的过程与 Java 中的方式完全相同。
你还可以将属性的 getter 和 setter 标记为 external
:
在幕后,这将创建两个函数 getMyProperty
和 setMyProperty
,并且都被标记为 external
。
在 Kotlin 中使用 Lombok 生成的声明
你可以在 Kotlin 代码中使用 Java 的 Lombok 生成的声明。 如果你需要在同一个混合的 Java/Kotlin 模块中生成并使用这些声明,你可以在 Lombok 编译器插件页面 学习如何做到这一点。 如果你从另一个模块调用这些声明,则不需要使用此插件来编译该模块。