Kotlin 设计核心目标之一:无缝和 Java 互操作,可以混编、互相调用,共享类库。
前提:Kotlin 编译成标准 JVM 字节码,和 Java 运行在同一个虚拟机。
一、基础原则
- Kotlin 可以直接调用所有 Java 代码;
- Java 可以调用 Kotlin 代码,但需要理解 Kotlin 语法映射成的 Java 签名;
- 无性能损耗,同一套字节码;
- 存在语言特性差异带来的互操作坑(空安全、函数、属性、伴生对象等)。
二、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) → KotlinMutableList!
不要直接赋值给只读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(),两种方式:
- 函数上加
@JvmStatic
companion object {
@JvmStatic
fun create(): User = User()
}
// Java: User.create()
-
@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? 在字节码层面就是 String,JVM 没有可空标记
空安全只是 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>)
六、常见面试问答精简版
Kotlin 调用 Java 最大风险?
平台类型T!,编译器不做空检查,极易 NPE,建议 Java 添加@Nullable。Java 怎么直接调用伴生对象方法不用写 Companion?
方法加@JvmStatic。Kotlin 默认参数 Java 不能使用怎么办?
添加@JvmOverloads生成重载。扩展函数在 Java 如何调用?
编译为静态方法,在对应的XXKt类中调用。@JvmField作用?
跳过 getter/setter,直接对外暴露字段。顶层函数 Java 找不到?
顶层函数放在文件名Kt类;可用@file:JvmName修改类名。
七、典型坑清单
- 平台类型引发隐性 NPE
- 忘记
@JvmStatic,Java 必须写.Companion - 默认参数不加
@JvmOverloads,Java 无法省略参数 - 扩展函数误以为能像实例方法在 Java 调用
- Kotlin 只读 List 实际接收 Java MutableList,外部依然能修改
- 泛型自动生成通配符导致 Java 编译不兼容(用 @JvmSuppressWildcards)