文章目录
- [第 1 章 混合工程的第一道墙:平台类型与 API 边界](#第 1 章 混合工程的第一道墙:平台类型与 API 边界)
- [第 2 章 @JvmStatic、@JvmField、@JvmOverloads:Java 能调到的形状](#第 2 章 @JvmStatic、@JvmField、@JvmOverloads:Java 能调到的形状)
- [第 3 章 SAM 转换与函数类型:lambda 在 Java 侧的落点](#第 3 章 SAM 转换与函数类型:lambda 在 Java 侧的落点)
- [第 4 章 @Nullable、@NotNull 与跨语言 null 契约](#第 4 章 @Nullable、@NotNull 与跨语言 null 契约)
- [第 5 章 suspend 的 Java 视角:Continuation 与调用边界](#第 5 章 suspend 的 Java 视角:Continuation 与调用边界)
- [第 6 章 API 边界治理:模块分层与混编反模式](#第 6 章 API 边界治理:模块分层与混编反模式)
- [面试速查 · 追问链](#面试速查 · 追问链)
-
-
- [追问链 #1:什么是平台类型 `T!`? 🔥](#1:什么是平台类型
T!? 🔥) - [追问链 #2:@JvmStatic、@JvmField、@JvmOverloads 分别解决什么? 🔥](#2:@JvmStatic、@JvmField、@JvmOverloads 分别解决什么? 🔥)
- [追问链 #3:Kotlin lambda 什么情况下 Java 不能用? ⭐](#3:Kotlin lambda 什么情况下 Java 不能用? ⭐)
- [追问链 #4:Java 能直接调用 suspend 函数吗? ⭐](#4:Java 能直接调用 suspend 函数吗? ⭐)
- [追问链 #5:@Nullable 和 Kotlin `T?` 怎么配合? 💡](#5:@Nullable 和 Kotlin
T?怎么配合? 💡)
- [追问链 #1:什么是平台类型 `T!`? 🔥](#1:什么是平台类型
-
- 完整链路一句通
- 相关推荐
第 1 章 混合工程的第一道墙:平台类型与 API 边界
Gradle 里 kotlin 与 java 源码目录并存时,真正容易出线上事故的不是「语法差异」,而是类型契约在语言边界上断裂 。Kotlin 编译器对 Java 声明的类型无法完全推断可空性,于是产生平台类型 (platform type),在 IDE 里常写作 T!------对 Kotlin 而言「可空也可非空」,编译器不强制 你处理 null,但 JVM 运行时照样 NullPointerException。
典型边界有三类:
- Android Framework / Jetpack :
Bundle.getString()、SavedStateHandle.get<T>()等 Java 或带平台类型的 API。 - 三方 Java SDK :Retrofit 接口、OkHttp
Callback、旧版 Gson 模型。 - 团队遗留 Java 模块 :Kotlin 新代码调用 Java
Utils,或 Java Activity 调用 KotlinViewModel工厂。
工程原则:在边界收口,不在 UI 深处赌运气 。从 Java 进来的值,在 Repository / ViewModel 构造或映射层立刻收窄为 T 或 T?;向外暴露给 Java 的 Kotlin API,用注解与 @Jvm* 把契约写进字节码。
kotlin
// Java 侧声明:String getName();
// Kotlin 读到的是 String! ------ 必须主动决策
fun fromJavaBundle(bundle: Bundle): String {
val raw: String? = bundle.getString("title") // 显式收窄为 String?
return raw ?: ""
}
ViewModel 从 SavedStateHandle 读取导航参数,是 Android 工程里平台类型边界的标准样本:
28:30:app/src/main/kotlin/com/kuen/beautifulchina/ui/viewmodel/ExploreViewModel.kt
private val category: Category? = savedStateHandle.get<String>("category")?.let { name ->
runCatching { Category.valueOf(name) }.getOrNull()
}
链路解读 :get<String> 在 Kotlin 侧为平台类型边界;?.let 把「可能缺失」写成 Category?;runCatching 处理非法枚举名。若写成 get<String>("category")!! 或 Category.valueOf(...) 裸调,Java 边界上的 null 或脏数据会直接变成崩溃。
原理补充
平台类型只存在于 Kotlin 源码对 Java 声明的引用 ;同一符号在 Java 文件里就是普通 T 或带 @Nullable 的 T。K2 编译器仍按相同规则生成 JVM 签名,互操作语义未变,差异主要在类型检查与 IDE 提示精度。
踩坑
- 把
String!当String一路传到 UI,省略边界校验。 - 在 Java 公共 API 返回 null 却不标
@Nullable,Kotlin 调用方无法获得可空提示。 - 混编模块「默认都能互相调」------未划分谁负责收窄平台类型。
动手练
- A :用一句话说明
String!与String?在编译器检查上的区别。 - D :在所在工程搜索
getString或SavedStateHandle.get,找 1 处并判断:若去掉?.谁会先 NPE。
第 2 章 @JvmStatic、@JvmField、@JvmOverloads:Java 能调到的形状
Kotlin 的 object、companion object、默认参数、顶层函数,编译到 JVM 后与 Java 开发者直觉不完全一致。三类注解负责把** Kotlin 写法翻译成 Java 调用面**。
| 注解 | 解决的问题 | Java 调用侧效果 |
|---|---|---|
@JvmStatic |
companion / object 成员默认是实例方法 |
生成真正静态方法 |
@JvmField |
Kotlin val/var 生成 getter/setter |
暴露为 public 字段 |
@JvmOverloads |
默认参数只生成一个完整签名 | 生成多重重载,逐参省略 |
@JvmStatic :Java 侧 Logger.d 式工具类、单例工厂常用。
kotlin
object NetworkDefaults {
const val CONNECT_TIMEOUT_SEC = 10
@JvmStatic
fun tag(module: String): String = "AppNet/$module"
}
class ApiFactory private constructor() {
companion object {
@JvmStatic
fun create(baseUrl: String): Retrofit = /* ... */
}
}
Java 调用:NetworkDefaults.tag("home")、ApiFactory.create(url)。无 @JvmStatic 时 Java 需写 NetworkDefaults.INSTANCE.tag(...),团队混编库几乎不可接受。
@JvmField :Parcelable 风格常量、与 Java 框架反射读字段的场景;不要 滥用------破坏封装时优先 @JvmStatic getter。
kotlin
class EventBusTags {
@JvmField
val refreshList: String = "refresh_list"
}
@JvmOverloads:Kotlin 默认参数在 JVM 上只保留「全参」构造;Java 无法省略尾部参数。
kotlin
class ImageLoader @JvmOverloads constructor(
context: Context,
private val diskCacheMb: Int = 50,
private val crossfade: Boolean = true,
) {
@JvmOverloads
fun load(url: String, placeholder: Int = R.drawable.placeholder) { /* ... */ }
}
Java 可 new ImageLoader(ctx) 或 loader.load(url);Kotlin 仍享受单一定义。
原理补充
@JvmOverloads 生成重载顺序从右向左省略默认参数;与 @JvmName 配合可解决签名冲突(同名擦除),本篇不展开。const val 已是编译期常量,Java 用 NetworkDefaults.CONNECT_TIMEOUT_SEC 直接访问,无需 @JvmField。
踩坑
- 给
data class主构造加@JvmOverloads却不考虑copy与 Java Bean 工具链的期望形状。 @JvmField暴露可变var,Java 任意改写破坏不可变模型。companion里只有@JvmStatic方法、无const,Java 仍要类名前缀------与 Javastatic工具类习惯不一致时漏注解。
动手练
- A :
companion object里fun parse(json: String)要给 Java 静态调用,加哪条注解? - B(起点):下面 Java 调用编译失败,补注解并说明原因:
kotlin
object RouteTable {
fun deepLink(path: String, animated: Boolean = true): Uri { /* ... */ }
}
// Java: RouteTable.deepLink("/home");
第 3 章 SAM 转换与函数类型:lambda 在 Java 侧的落点
Kotlin 对函数式接口 (仅一个抽象方法的 interface)自动做 SAM 转换:lambda 可替代匿名类。Java 8+ 同样支持 lambda,但 Kotlin 与 Java 在函数类型 、内联 lambda 上并不对称。
Kotlin 定义 SAM (推荐 fun interface,K2 下语义清晰):
kotlin
fun interface OnItemClick {
fun onClick(id: String)
}
fun setListener(listener: OnItemClick) { /* ... */ }
// Kotlin 调用
setListener { id -> navigate(id) }
java
// Java 调用 ------ 与 Kotlin lambda 同一 SAM
setListener(id -> navigate(id));
Java 定义 SAM,Kotlin 调用:
java
public interface RetryPolicy {
boolean shouldRetry(int attempt, IOException e);
}
kotlin
val policy = RetryPolicy { attempt, e -> attempt < 3 && e !is SSLException }
函数类型 () -> Unit 不是 SAM :若 API 参数是 Kotlin 函数类型而非 fun interface,Java 侧不能写 lambda,需 Function0<Unit> 或改成 fun interface。
Retrofit Callback、OkHttp 拦截器链是工程里最常见的 Java SAM 边界。Kotlin 侧用 lambda 写很爽,但要意识到异常与 null 契约仍按 Java 规则:
36:46:app/src/main/kotlin/com/kuen/beautifulchina/data/remote/ScenerySupplementManager.kt
private val retrofit: Retrofit by lazy {
val client = OkHttpClient.Builder()
.connectTimeout(6, TimeUnit.SECONDS)
.readTimeout(6, TimeUnit.SECONDS)
.build()
Retrofit.Builder()
.baseUrl("https://shuaici.github.io/")
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.build()
}
此处 Retrofit/OkHttp 全是 Java API;Kotlin 只是在边界内配置 Builder。若把 Callback 包一层 Kotlin suspend,应在 Repository 内收口,而不是让 Java 模块直接依赖 Continuation。
原理补充
SAM 转换在编译期生成 invokedynamic 或匿名类;fun interface 显式标记 SAM,避免多抽象方法接口误用。内联高阶函数(inline fun)的 lambda 不能 稳定传递给 Java 存储,混编 API 面应使用非 inline 的 fun interface 或接口类型。
踩坑
- 把
(T) -> R当公共 API 暴露给 Java 模块------Java 无 SAM 转换。 - Kotlin lambda 捕获可变
var,在 Java 回调异步触发时读到过期值。 - 对 Java
Runnable传{ }却期望返回值的映射逻辑------Unit与void混淆。
动手练
- C :定义
fun interface Validator,实现validate(input: String): Boolean,并写 Kotlin / Java 各一行调用骨架。 - B(起点):将 Java 匿名类改为 Kotlin SAM lambda:
java
button.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) { submit(); }
});
第 4 章 @Nullable、@NotNull 与跨语言 null 契约
仅靠 Kotlin 的 T? 无法让 Java 调用方「看见」可空性;需要在 JVM 字节码上携带 JSR-305 / Jetpack 可空注解 ,Kotlin 编译器才能把 Java API 读成 T 或 T?,而不是 T!。
常用注解(Android 工程优先 Jetpack):
| 注解 | 含义 | Kotlin 侧读取 |
|---|---|---|
@Nullable |
允许 null | T? |
@NotNull |
禁止 null | T |
| 无注解 | 未知 | 平台类型 T! |
Kotlin 暴露给 Java:
kotlin
import androidx.annotation.Nullable
import androidx.annotation.NonNull
class UserBridge {
@Nullable
fun findName(id: String): String? = repository.find(id)?.name
@NonNull
fun requireName(id: String): String =
findName(id) ?: throw IllegalArgumentException("missing $id")
}
在 Kotlin 文件里为 Java 互操作设默认:
kotlin
@file:JvmName("OrderCompat")
@file:JvmDefaultWithoutCompatibility
package com.example.order
@JvmDefaultWithoutCompatibility 影响接口默认实现与 Java 二进制兼容,适用于库作者;App 模块更常见的是对对外模块 的 public API 补齐 @Nullable/@NonNull。
与 Kotlin 空安全协同 :从带 @Nullable 的 Java 方法返回值赋给 Kotlin val x: String 会编译报错,迫使在边界处理:
kotlin
fun bindTitle(javaApi: LegacyApi) {
val title: String = javaApi.getTitle() // 若 getTitle 标 @Nullable,应改为 String?
// ...
}
团队规范建议:新 Kotlin public API 一律显式 T?/T;供 Java 调用的模块同步加 Jetpack 注解;遗留 Java 无注解 API 在 Adapter 层一次性收窄。
原理补充
Kotlin 编译器内置对 Android、org.jetbrains.annotations、javax.annotation 等可空注解的识别表。混编时开启 -Xjsr305=strict(或项目默认的 strict 模式)可把无注解 Java 类型当更宽松平台类型,具体以模块 build.gradle.kts 为准。
踩坑
- Kotlin 返回
String?却未标@Nullable,Java 调用方静态分析工具仍当非空。 - 混用
javax.annotation与androidx.annotation,部分工具链只认其一。 - 在 Kotlin 内部用
!!「帮 Java 兜底」,把契约推迟到崩溃栈。
动手练
- A :Java 方法
String getNote()实际可能返回 null,Kotlin 侧应要求加哪个注解、读到什么类型? - D :搜索工程里
androidx.annotation.Nullable或 Java 源文件无注解的 getter,列 1 处并给出 Kotlin 边界收窄写法。
第 5 章 suspend 的 Java 视角:Continuation 与调用边界
suspend 函数是 Kotlin 协程的核心,但 JVM 字节码上不是「多一个 async 关键字」 ,而是带有隐藏 Continuation 参数的状态机。Java 不能 像 Kotlin 一样直接 await loadUser(),这是混编里最容易被误解的一点。
Kotlin 编译后(概念性):
kotlin
suspend fun loadUser(id: String): User
// 近似 JVM 签名:
// Object loadUser(String id, Continuation<? super User> continuation)
因此:
- Java 调用 Kotlin
suspend:应使用kotlinx-coroutines-core提供的适配(如FutureKt.future、BuildersKt等),或在 Kotlin 侧提供 非 suspend 包装 给 Java。 - Kotlin 调用 Java :普通阻塞 API 用
withContext(Dispatchers.IO);勿假设 Java 回调能自动变suspend,需suspendCancellableCoroutine手动挂起。
给 Java 模块的推荐包装:
kotlin
class UserRepository(private val api: UserApi) {
suspend fun loadUser(id: String): User = api.fetch(id).let(::mapUser)
// Java 调用面:显式阻塞或 Future,不把 Continuation 泄漏出去
@JvmOverloads
fun loadUserBlocking(id: String, timeoutMs: Long = 5_000): User? =
runBlocking {
withTimeout(timeoutMs) { loadUser(id) }
}
}
生产环境更推荐 Kotlin 协程层完成异步,Java 只接回调或 ListenableFuture (kotlinx-coroutines-guava),避免在 UI 线程调用 runBlocking。
从 Java 回调到 suspend(Repository 内收口):
kotlin
suspend fun fetchLegacy(): Data = suspendCancellableCoroutine { cont ->
legacyJavaClient.load(object : LegacyCallback {
override fun onSuccess(value: Data) {
if (cont.isActive) cont.resume(value)
}
override fun onError(t: Throwable) {
if (cont.isActive) cont.resumeWithException(t)
}
})
}
ViewModel 层继续 viewModelScope.launch { },Java Activity 若仍存在,只观察 LiveData/StateFlow 的 Java 友好暴露(asLiveData()),而不是直接调 suspend。
原理补充
K2 下 suspend 状态机与 CPS 变换稳定;@JvmSuppressWildcards / @JvmWildcard 影响泛型在 Java 的可调用性,与 Continuation 泛型协变一起决定库 API 是否「Java 友好」。本篇记住:suspend 是 Kotlin 语法,不是 Java 一等公民。
踩坑
- Java 代码直接调
suspend fun,编译器报缺少Continuation参数。 - 在
runBlocking里调 UI 相关suspend,主线程死锁。 - 把
suspendCancellableCoroutine泄漏到 public API,Java 侧无法完成。
动手练
- A :用一句话向 Java 同事解释为什么不能
userRepo.loadUser("1");直接拿User。 - C :为已有
suspend fun loadProvince(id: String): Province写fun interface回调包装loadProvinceAsync(id, onResult: (Province?) -> Unit)的 Kotlin 骨架(5 行内,内部CoroutineScope启动即可)。
第 6 章 API 边界治理:模块分层与混编反模式
互操作能力齐全后,Senior 工程师关注的是边界画在哪 :不是处处 @JvmStatic,而是让 Java 与 Kotlin 各写擅长的层,契约在接口上闭合。
推荐分层:
text
Java 遗留 UI / 老 SDK 适配
↓ 平台类型 / @Nullable 收窄
Kotlin Domain + Repository(suspend、Flow、data class)
↓ @JvmStatic 工具、@JvmOverloads 构造
对外 SDK / 供 Java 模块调用的 Facade
Facade 模式示例:
kotlin
object ProvinceFacade {
@JvmStatic
fun loadDisplayName(
context: Context,
provinceId: String,
callback: OnNameLoaded,
) {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
scope.launch {
val name = runCatching {
withContext(Dispatchers.IO) { repository.load(provinceId).nameCn }
}.getOrNull()
callback.onLoaded(name)
}
}
fun interface OnNameLoaded {
fun onLoaded(name: String?)
}
}
Java 老页面只依赖 ProvinceFacade 与 OnNameLoaded,不碰 suspend 与 StateFlow。
反模式清单:
| 反模式 | 问题 | 改法 |
|---|---|---|
| 平台类型穿透 UI | 随机 NPE | 映射层 ?: / let 收窄 |
处处 runBlocking 桥接 |
阻塞与死锁 | Facade + 主线程回调 |
| public API 用函数类型 | Java 无法 lambda | fun interface |
无 @Nullable 的 Java 返回 |
Kotlin 当非空用 | 补注解或 Adapter 校验 |
!! 处理 Java 边界 |
契约失效 | 显式 T? + 降级 |
动手练
- B(起点):下面混编代码有三处边界问题,列出并改写:
kotlin
class BadBridge(private val bundle: Bundle) {
fun title(): String = bundle.getString("title")!! // ①
fun load(cb: (String) -> Unit) { /* ... */ } // ② Java 调不了
suspend fun refresh() { /* ... */ } // ③ Java 直接调
}
- D 验收清单 :选 1 个 Kotlin
object工具类,确认 Java 模块能否静态调用;若不能,补@JvmStatic并记录调用前后差异。
面试速查 · 追问链
追问链 #1:什么是平台类型 T!? 🔥
标准回答 (≤200 字):平台类型是 Kotlin 引用 Java(或无 null 注解)声明时的特殊类型,对编译器而言可空也可非空,写作 T!。编译器不强制 null 检查,运行时仍可能 NPE。应在 Repository/ViewModel 边界立刻收窄为 T 或 T?,不要穿透到 UI。
追问 1 :SavedStateHandle.get<String>() 在 Kotlin 里算什么?
答 :来自 Java/Android API 的平台类型边界;应 get<String>(...)?.let 或赋给 String? 再处理,避免 !!。
追问 2 :Kotlin 返回 String? 给 Java 要注意什么?
答 :同步加 @Nullable,否则 Java 静态分析与 Kotlin 契约不一致。
追问链 #2:@JvmStatic、@JvmField、@JvmOverloads 分别解决什么? 🔥
标准回答 (≤200 字):@JvmStatic 让 companion/object 成员在 Java 里是真正静态方法;@JvmField 暴露 public 字段,跳过 getter;@JvmOverloads 为带默认参数的构造/方法生成 Java 重载。混编库对外工具类几乎必备 @JvmStatic。
追问 1 :Kotlin 默认参数 Java 为什么调不了?
答 :JVM 只保留全参签名;需 @JvmOverloads 或 Java 侧手动传齐参数。
追问 2 :const val 需要 @JvmField 吗?
答 :不需要,const val 已是编译期常量,Java 直接用类名访问。
追问链 #3:Kotlin lambda 什么情况下 Java 不能用? ⭐
标准回答 (≤200 字):参数是 Kotlin 函数类型(如 (String) -> Unit)时,Java 不能 SAM 转换,需 Function1 等或改成 fun interface。只有 Java 函数式接口或 Kotlin fun interface 才支持 lambda 互调。内联高阶函数的 lambda 不宜存给 Java 字段。
追问 1 :fun interface 和普通 interface 对 Java 有何差别?
答 :fun interface 明确单抽象方法,Kotlin/Java 两侧都可 lambda;多方法接口必须匿名类。
追问 2 :Retrofit Callback 在 Kotlin 里怎么写?
答:可用 SAM lambda,但应在 Repository 层封装,避免 Java 模块依赖协程细节。
追问链 #4:Java 能直接调用 suspend 函数吗? ⭐
标准回答 (≤200 字):不能。suspend 编译后带 Continuation 参数,是 Kotlin 协程 CPS 形态。Java 应调包装方法:runBlocking(慎用)、Future/ListenableFuture 适配,或回调/fun interface Facade。Kotlin 侧用 suspendCancellableCoroutine 把 Java 回调挂起,仅在 Repository 内。
追问 1 :为什么主线程 runBlocking 危险?
答:阻塞 UI 线程,且与已挂起的协程争用同一线程可能死锁。
追问 2 :老 Java Activity 如何接 Kotlin StateFlow?
答 :stateFlow.asLiveData() 或在 Kotlin Facade 里转回调,不让 Java 直接碰 suspend。
追问链 #5:@Nullable 和 Kotlin T? 怎么配合? 💡
标准回答 (≤200 字):Java API 标 @Nullable/@NotNull 后,Kotlin 编译器把返回类型读成 T?/T,减少平台类型。Kotlin 对外 public API 也应补 Jetpack 注解,方便 Java 与静态分析。无注解的遗留 Java 返回视作 T!,在 Adapter 一次性校验。
追问 1 :-Xjsr305=strict 有什么效果?
答:对 JSR-305 注解更严格,影响无注解 Java 类型的可空推断策略,以模块配置为准。
追问 2 :!! 和边界校验哪个更适合混编?
答 :边界校验 + 显式 T?;!! 只用于逻辑上不可能 null 且可测试覆盖的点。
完整链路一句通
Java 进来的平台类型在边界收窄为 T? → 对外 API 用 @JvmStatic/@JvmOverloads/@JvmField 塑 Java 调用面 → SAM 与 fun interface 承载 lambda → @Nullable/@NotNull 写清 null 契约 → suspend 仅在 Kotlin 层,Java 走 Facade/回调/Future → Repository 收口,不让混编细节穿透 UI。
