Java 与 Kotlin 互操作
问题
Java 和 Kotlin 如何互相调用?互操作中有哪些常见坑?
答案
1. Kotlin 调用 Java
大多数情况下 Kotlin 调用 Java 代码是无缝的,但需要注意平台类型问题:
// Java 代码
public class JavaUtils {
public static String getName() { return null; }
public static List<String> getList() { return Arrays.asList("a", "b"); }
}
// Kotlin 调用
val name = JavaUtils.getName() // 类型推断为 String!(平台类型)
println(name.length) // ⚠️ 运行时 NPE!
// 安全做法:显式声明为可空类型
val safeName: String? = JavaUtils.getName()
println(safeName?.length) // 安全
平台类型(Platform Types)
Java 返回值在 Kotlin 中显示为 String!(带感叹号),表示编译器不知道是否可空。你可以当作 String 或 String? 使用,但如果 Java 返回 null 而你当作非空使用,就会 NPE。
最佳实践:Java 库应使用 @Nullable/@NonNull 注解,Kotlin 端显式声明为可空类型。
2. Java 调用 Kotlin
Kotlin 代码编译为标准 Java 字节码,但某些特性需要注解才能在 Java 端友好使用:
// Kotlin 文件 Utils.kt
object AppConfig {
const val VERSION = "1.0" // 编译为 public static final
@JvmStatic // 生成真正的 static 方法
fun getAppName() = "MyApp"
fun getDebug() = false
}
// 顶层函数
fun formatDate(date: Date): String = SimpleDateFormat("yyyy-MM-dd").format(date)
// Java 调用
String version = AppConfig.VERSION; // ✅ const 直接访问
String name = AppConfig.getAppName(); // ✅ @JvmStatic
boolean debug = AppConfig.INSTANCE.getDebug(); // 无 @JvmStatic 需要通过 INSTANCE
// 顶层函数 —— 编译为 UtilsKt.formatDate()
String date = UtilsKt.formatDate(new Date());
3. 常用互操作注解
| 注解 | 作用 | 使用场景 |
|---|---|---|
@JvmStatic | 生成静态方法 | companion object / object 中的方法 |
@JvmField | 暴露为公开字段(不生成 getter/setter) | Java 直接访问字段 |
@JvmOverloads | 为默认参数生成重载方法 | Java 调用有默认参数的函数 |
@JvmName | 自定义编译后的类名或方法名 | 解决名称冲突、美化 API |
@Throws | 声明检查异常 | Java 调用可能抛异常的 Kotlin 函数 |
@JvmStatic | 生成真正的 static 方法 | Android 中 @BindingAdapter 等框架注解 |
class UserApi {
// @JvmOverloads —— 为 Java 生成 3 个重载方法
@JvmOverloads
fun createUser(
name: String,
age: Int = 0,
email: String = ""
) { /* ... */ }
// @JvmField —— 直接暴露字段
@JvmField
val TAG = "UserApi"
// @Throws —— 声明抛出异常
@Throws(IOException::class)
fun loadData(): String { /* ... */ }
}
// Java 调用
UserApi api = new UserApi();
api.createUser("Alice"); // ✅ @JvmOverloads 生成的重载
api.createUser("Alice", 25); // ✅
api.createUser("Alice", 25, "a@b.c"); // ✅
String tag = api.TAG; // ✅ @JvmField 直接访问
try {
api.loadData(); // ✅ @Throws 让 Java 知道要 try-catch
} catch (IOException e) { /* ... */ }
4. 集合互操作
// Kotlin 只读集合 → Java 可修改!
val list: List<String> = listOf("a", "b")
// Java 端收到的是 java.util.List,可以调用 add()
// 安全做法:传递不可变副本
fun getItems(): List<String> = Collections.unmodifiableList(items)
| Kotlin 类型 | Java 类型 | 注意 |
|---|---|---|
List<T> | java.util.List<T> | Java 端可以修改! |
MutableList<T> | java.util.List<T> | 同一个类型 |
Map<K, V> | java.util.Map<K, V> | Java 端可以修改! |
5. SAM 转换
Kotlin 可以将 Lambda 传递给 Java 的单抽象方法(SAM)接口:
// Java 接口
public interface OnClickListener {
void onClick(View v);
}
// Kotlin 调用 —— SAM 转换,Lambda 自动转为接口实例
button.setOnClickListener { view ->
// 处理点击
}
// 等价于
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
// 处理点击
}
})
Kotlin 接口的 SAM 转换
Kotlin 1.4+ 支持 fun interface(函数式接口),使 Kotlin 接口也支持 SAM 转换:
fun interface Transformer {
fun transform(input: String): String
}
fun applyTransform(t: Transformer) = t.transform("hello")
applyTransform { it.uppercase() } // SAM 转换
6. 空安全互操作策略
// 策略 1:使用平台类型注解(推荐 Java 库维护者)
// Java 端
@NonNull
public static String getName() { return "Alice"; }
@Nullable
public static String getNickname() { return null; }
// 策略 2:Kotlin 端防御性编程
val name: String = JavaUtils.getName() ?: "default" // 安全默认值
val list: List<String> = JavaUtils.getList().orEmpty() // 空安全
// 策略 3:使用 !! 断言(仅在确定非空时)
val name: String = JavaUtils.getName()!! // 非空断言,null 时抛 NPE
常见面试问题
Q1: Kotlin 编译后的 class 文件和 Java 有什么区别?
答案:
Kotlin 编译为标准 Java 字节码(.class 文件),与 Java 编译结果格式相同,都运行在 JVM 上。主要区别:
- 文件名:
Utils.kt→UtilsKt.class(顶层函数的容器类) - 空安全:Kotlin 编译器会插入空检查代码(
Intrinsics.checkNotNullParameter) - 属性:
var name→ private field + getter/setter 方法 - data class:自动生成
equals、hashCode、toString、copy、componentN - 协程:
suspend函数编译为带Continuation参数的普通方法 + 状态机
Q2: 为什么 @JvmStatic 不能放在所有地方?
答案:
@JvmStatic 只能用在 companion object 或 object 声明中。因为它需要将方法放到外层类上作为 static 方法,如果是普通类的方法就没有意义(本身就是实例方法)。
Q3: Kotlin 的 Nothing 类型在 Java 互操作中表现如何?
答案:
Nothing 在 JVM 上被擦除为 Void。它表示"永远不会正常返回"的类型:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
在泛型中,Nothing 是所有类型的子类型,所以 List<Nothing> 可以赋值给任何 List<T>。
Q4: 如何在混合项目中管理空安全?
答案:
- Java 端:使用 JSR 305 注解(
@Nullable/@NonNull)或 AndroidX 注解 - Kotlin 编译器配置:
-Xjsr305=strict将 Java 注解严格映射为 Kotlin 类型 - 模块边界:在 Java/Kotlin 交界处定义清晰的 API 契约
- 渐进式迁移:先给最常跨语言调用的 Java 类加注解
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
}
}
Q5: Java 的 static 成员在 Kotlin 中如何访问?
答案:
直接通过类名访问,与 Java 相同:
// Java: Math.max(1, 2)
val max = Math.max(1, 2) // ✅ 直接调用
// Java: Integer.MAX_VALUE
val maxInt = Int.MAX_VALUE // ✅ Kotlin 基本类型映射
// Java: Collections.emptyList()
val empty = Collections.emptyList<String>() // ✅
Kotlin 没有 static 关键字,用 companion object + @JvmStatic 替代。