Kotlin 与 java 的互操作性

Kotlin 设计核心目标之一:无缝和 Java 互操作,可以混编、互相调用,共享类库。

前提:Kotlin 编译成标准 JVM 字节码,和 Java 运行在同一个虚拟机。

一、基础原则

  1. Kotlin 可以直接调用所有 Java 代码;
  2. Java 可以调用 Kotlin 代码,但需要理解 Kotlin 语法映射成的 Java 签名;
  3. 无性能损耗,同一套字节码;
  4. 存在语言特性差异带来的互操作坑(空安全、函数、属性、伴生对象等)。

二、Kotlin 调用 Java

1. 空安全问题(最常见考点)

Java 没有可空类型,所有引用类型 Kotlin 都会识别为平台类型 !

  • String!:既可以当作可空 String?,也可以非空 String
  • 风险:Kotlin 编译器不做空检查,运行时容易 NPE
// Java
public String getName() {
    return null;
}
val name: String = javaObj.getName() // 编译不报错,运行NPE!
val name: String? = javaObj.getName() // 安全写法

最佳实践:尽可能给 Java 方法加上 @Nullable / @NotNull,Kotlin 会正确识别可空性。

2. Java 集合 → Kotlin

Java 原生集合都是可变集合

  • List(Java) → Kotlin MutableList!
    不要直接赋值给只读 List,否则运行时依然可以修改。

3. Java void 返回

Kotlin 中接收为 Unit

4. SAM 转换(单抽象方法接口)

Java 函数式接口可以直接用 Kotlin lambda 简写

public interface Runnable { void run(); }
thread(Runnable { println("hello") })
// SAM 简化
thread { println("hello") }

注意:Kotlin 自己定义的函数接口需要 fun interface 才能 SAM 转换。

5. 检查异常

Java 受检异常(checked exception)在 Kotlin 中不强制捕获

public void read() throws IOException {}
javaObj.read() // 编译器不要求 try-catch,运行时依然会抛出

三、Java 调用 Kotlin(重点,坑最多)

Kotlin 很多语法糖编译后有特殊规则,Java 不能直接按 Kotlin 写法调用。

1. 属性(val / var)

Kotlin 属性编译生成:getXxx() / setXxx()

var name: String = "test"
val age: Int = 18

Java 调用:

obj.getName();
obj.setName("abc");
obj.getAge();
  • 不能直接访问 name 字段(默认私有)
  • 如果想要暴露字段给 Java:添加 @JvmField
@JvmField
var name = "test"
// Java: obj.name 直接访问

2. 顶层函数 / 顶层属性

Kotlin 文件 User.kt 中直接写顶层函数:

fun hello() {}

编译生成类:UserKt
Java 调用:

UserKt.hello();

自定义类名:@JvmName("UserUtils")

@file:JvmName("UserUtils")
fun hello(){}
// Java UserUtils.hello();

3. 伴生对象 companion object

class User {
    companion object {
        fun create(): User = User()
    }
}

默认 Java 调用方式:

User.Companion.create();

想要直接 User.create(),两种方式:

  1. 函数上加 @JvmStatic
companion object {
    @JvmStatic
    fun create(): User = User()
}
// Java: User.create()
  1. @JvmStatic 同样支持伴生对象属性

4. 单例 object(对象声明)

object AppConfig {
    fun getVersion() = "1.0"
}

Java 调用:

AppConfig.INSTANCE.getVersion();

同样可用 @JvmStatic 简化:

object AppConfig {
    @JvmStatic
    fun getVersion() = "1.0"
}
// Java AppConfig.getVersion();

5. 默认参数

Kotlin 默认参数 Java 无法感知

fun login(name: String, pwd: String = "123456"){}

Java 只能调用完整参数版本,不能省略 pwd。

解决方案:@JvmOverloads 自动生成多个重载方法

@JvmOverloads
fun login(name: String, pwd: String = "123456"){}
// Java 存在 login(String) / login(String,String) 两个重载

⚠️ 构造函数同样支持 @JvmOverloads

6. 命名参数

Java 不支持,无法使用。

7. 扩展函数

扩展函数本质:静态工具方法,第一个参数为接收者

fun String.isEmail(): Boolean = ...

Java 调用:

StringsKt.isEmail("xxx@xx.com");

扩展函数不能当成实例方法调用

8. 可空类型

Kotlin String? 在字节码层面就是 StringJVM 没有可空标记
空安全只是 Kotlin 编译器检查,Java 调用可以随意传 null。

9. 枚举类

互操作基本无坑,和 Java 枚举一致。

10. 数据类 data class

自动生成 equals/hashCode/toString/copy,Java 正常调用。
注意:copy() 在 Java 可用,但解构声明只在 Kotlin 使用。

四、关键注解汇总(互操作必备)

注解 作用场景
@JvmStatic 伴生对象/object 中的方法,暴露成静态方法
@JvmField 属性直接公开为字段,不通过 getter/setter
@JvmOverloads 默认参数生成多套重载供 Java 调用
@JvmName 修改顶层函数/文件编译后的类名、方法名
@JvmSuppressWildcards 抑制泛型通配符,解决 Java 泛型 ? 互操作麻烦
@JvmInline 值类(inline class),优化基础类型包装

五、泛型互操作痛点

Kotlin 协变 List<out T> 映射 Java List<? extends T>
逆变 List<in T>List<? super T>
经常出现 Java 泛型通配符不匹配编译报错。
可以使用 @JvmSuppressWildcards 消除通配符。

fun test(list: List<@JvmSuppressWildcards String>)

六、常见面试问答精简版

  1. Kotlin 调用 Java 最大风险?
    平台类型 T!,编译器不做空检查,极易 NPE,建议 Java 添加 @Nullable

  2. Java 怎么直接调用伴生对象方法不用写 Companion?
    方法加 @JvmStatic

  3. Kotlin 默认参数 Java 不能使用怎么办?
    添加 @JvmOverloads 生成重载。

  4. 扩展函数在 Java 如何调用?
    编译为静态方法,在对应的 XXKt 类中调用。

  5. @JvmField 作用?
    跳过 getter/setter,直接对外暴露字段。

  6. 顶层函数 Java 找不到?
    顶层函数放在 文件名Kt 类;可用 @file:JvmName 修改类名。

七、典型坑清单

  1. 平台类型引发隐性 NPE
  2. 忘记 @JvmStatic,Java 必须写 .Companion
  3. 默认参数不加 @JvmOverloads,Java 无法省略参数
  4. 扩展函数误以为能像实例方法在 Java 调用
  5. Kotlin 只读 List 实际接收 Java MutableList,外部依然能修改
  6. 泛型自动生成通配符导致 Java 编译不兼容(用 @JvmSuppressWildcards)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容