跳到主要内容

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!(带感叹号),表示编译器不知道是否可空。你可以当作 StringString? 使用,但如果 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 上。主要区别:

  1. 文件名Utils.ktUtilsKt.class(顶层函数的容器类)
  2. 空安全:Kotlin 编译器会插入空检查代码(Intrinsics.checkNotNullParameter
  3. 属性var name → private field + getter/setter 方法
  4. data class:自动生成 equalshashCodetoStringcopycomponentN
  5. 协程suspend 函数编译为带 Continuation 参数的普通方法 + 状态机

Q2: 为什么 @JvmStatic 不能放在所有地方?

答案

@JvmStatic 只能用在 companion objectobject 声明中。因为它需要将方法放到外层类上作为 static 方法,如果是普通类的方法就没有意义(本身就是实例方法)。

Q3: Kotlin 的 Nothing 类型在 Java 互操作中表现如何?

答案

Nothing 在 JVM 上被擦除为 Void。它表示"永远不会正常返回"的类型:

fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}

在泛型中,Nothing 是所有类型的子类型,所以 List<Nothing> 可以赋值给任何 List<T>

Q4: 如何在混合项目中管理空安全?

答案

  1. Java 端:使用 JSR 305 注解(@Nullable/@NonNull)或 AndroidX 注解
  2. Kotlin 编译器配置-Xjsr305=strict 将 Java 注解严格映射为 Kotlin 类型
  3. 模块边界:在 Java/Kotlin 交界处定义清晰的 API 契约
  4. 渐进式迁移:先给最常跨语言调用的 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 替代。

相关链接