Kotlin与Java互操作

栏目: Java · 发布时间: 6年前

内容简介:Kotlin与Java互操作

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

Kotlin调用Java

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提供助记符来表示他们:

  • 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),那么该注解类型自身将用于 检索精确的可空性,且具有与该可空性注解相同的含义。

@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.METHOD 用于方法的返回值;
  • ElementType.PARAMETER 用于值参数;
  • ElementType.FIELD 用于字段;
  • ElementType.TYPE_USE(自 1.1.60 起)适用于任何类型,包括类型参数、类型参数的上界与通配符类型。

当类型并未标注可空性注解时使用默认可空性,并且该默认值是由最内层标注有带有与所用类型相匹配的 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 注解

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

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

  • MigrationStatus.STRICT 使注解像任何纯可空性注解一样工作,即对不当用法报错并影响注解声明内的类型在 Kotlin中的呈现;
  • 对于 MigrationStatus.WARN,不当用法报为警告而不是错误; 但注解声明内的类型仍是平台类型;
  • MigrationStatus.IGNORE 则使编译器完全忽略可空性注解。

库的维护者还可以将 @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,那么使用默认类型限定符的状态。

返回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检查星投影的泛型类型。

if (a is List<Int>) // 错误:无法检查它是否真的是一个 Int 列表
// but
if (a is List<*>) // OK:不保证列表的内容

Java数组

与 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 可变参数

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 给一个声明为可变参数的方法。

SAM转换

就像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

Java 可以轻松调用 Kotlin 代码。

属性

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

  • getter方法,其名称通过加前缀get得到;
  • setter方法,其名称通过加前缀set得到(只适用于var属性);
  • 私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。

例如,将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 类的静态方法。

package demo
class Foo
fun bar() {
   println("这只是一个bar方法")
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

第一本Docker书 修订版

第一本Docker书 修订版

詹姆斯·特恩布尔 (James Turnbull) / 李兆海、刘斌、巨震 / 人民邮电出版社 / 2016-4-1 / CNY 59.00

Docker是一个开源的应用容器引擎,开发者可以利用Docker打包自己的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。 本书由Docker公司前服务与支持副总裁James Turnbull编写,是Docker开发指南。本书专注于Docker 1.9及以上版本,指导读者完成Docker的安装、部署、管理和扩展,带领读者经历从测试到生产的整个开发生......一起来看看 《第一本Docker书 修订版》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具