Kotlin 与 Java 互操作:混合工程里的平台类型与 API 边界
文章目录第 1 章 混合工程的第一道墙平台类型与 API 边界原理补充踩坑动手练第 2 章 JvmStatic、JvmField、JvmOverloadsJava 能调到的形状原理补充踩坑动手练第 3 章 SAM 转换与函数类型lambda 在 Java 侧的落点原理补充踩坑动手练第 4 章 Nullable、NotNull 与跨语言 null 契约原理补充踩坑动手练第 5 章 suspend 的 Java 视角Continuation 与调用边界原理补充踩坑动手练第 6 章 API 边界治理模块分层与混编反模式动手练面试速查 · 追问链追问链 #1什么是平台类型 T! 追问链 #2JvmStatic、JvmField、JvmOverloads 分别解决什么 追问链 #3Kotlin lambda 什么情况下 Java 不能用 ⭐追问链 #4Java 能直接调用 suspend 函数吗 ⭐追问链 #5Nullable 和 Kotlin T? 怎么配合 完整链路一句通相关推荐第 1 章 混合工程的第一道墙平台类型与 API 边界Gradle 里kotlin与java源码目录并存时真正容易出线上事故的不是「语法差异」而是类型契约在语言边界上断裂。Kotlin 编译器对 Java 声明的类型无法完全推断可空性于是产生平台类型platform type在 IDE 里常写作T!——对 Kotlin 而言「可空也可非空」编译器不强制你处理 null但 JVM 运行时照样NullPointerException。典型边界有三类Android Framework / JetpackBundle.getString()、SavedStateHandle.getT()等 Java 或带平台类型的 API。三方 Java SDKRetrofit 接口、OkHttpCallback、旧版 Gson 模型。团队遗留 Java 模块Kotlin 新代码调用 JavaUtils或 Java Activity 调用 KotlinViewModel工厂。工程原则在边界收口不在 UI 深处赌运气。从 Java 进来的值在 Repository / ViewModel 构造或映射层立刻收窄为T或T?向外暴露给 Java 的 Kotlin API用注解与Jvm*把契约写进字节码。// Java 侧声明String getName();// Kotlin 读到的是 String! —— 必须主动决策funfromJavaBundle(bundle:Bundle):String{valraw:String?bundle.getString(title)// 显式收窄为 String?returnraw?:}ViewModel 从SavedStateHandle读取导航参数是 Android 工程里平台类型边界的标准样本private val category: Category? savedStateHandle.getString(category)?.let { name - runCatching { Category.valueOf(name) }.getOrNull() }链路解读getString在 Kotlin 侧为平台类型边界?.let把「可能缺失」写成Category?runCatching处理非法枚举名。若写成getString(category)!!或Category.valueOf(...)裸调Java 边界上的 null 或脏数据会直接变成崩溃。原理补充平台类型只存在于Kotlin 源码对 Java 声明的引用同一符号在 Java 文件里就是普通T或带Nullable的T。K2 编译器仍按相同规则生成 JVM 签名互操作语义未变差异主要在类型检查与 IDE 提示精度。踩坑把String!当String一路传到 UI省略边界校验。在 Java 公共 API 返回 null 却不标NullableKotlin 调用方无法获得可空提示。混编模块「默认都能互相调」——未划分谁负责收窄平台类型。动手练A用一句话说明String!与String?在编译器检查上的区别。D在所在工程搜索getString或SavedStateHandle.get找 1 处并判断若去掉?.谁会先 NPE。第 2 章 JvmStatic、JvmField、JvmOverloadsJava 能调到的形状Kotlin 的object、companion object、默认参数、顶层函数编译到 JVM 后与 Java 开发者直觉不完全一致。三类注解负责把** Kotlin 写法翻译成Java 调用面**。注解解决的问题Java 调用侧效果JvmStaticcompanion/object成员默认是实例方法生成真正静态方法JvmFieldKotlinval/var生成 getter/setter暴露为 public 字段JvmOverloads默认参数只生成一个完整签名生成多重重载逐参省略JvmStaticJava 侧Logger.d式工具类、单例工厂常用。objectNetworkDefaults{constvalCONNECT_TIMEOUT_SEC10JvmStaticfuntag(module:String):StringAppNet/$module}classApiFactoryprivateconstructor(){companionobject{JvmStaticfuncreate(baseUrl:String):Retrofit/* ... */}}Java 调用NetworkDefaults.tag(home)、ApiFactory.create(url)。无JvmStatic时 Java 需写NetworkDefaults.INSTANCE.tag(...)团队混编库几乎不可接受。JvmFieldParcelable 风格常量、与 Java 框架反射读字段的场景不要滥用——破坏封装时优先JvmStaticgetter。classEventBusTags{JvmFieldvalrefreshList:Stringrefresh_list}JvmOverloadsKotlin 默认参数在 JVM 上只保留「全参」构造Java 无法省略尾部参数。classImageLoaderJvmOverloadsconstructor(context:Context,privatevaldiskCacheMb:Int50,privatevalcrossfade:Booleantrue,){JvmOverloadsfunload(url:String,placeholder:IntR.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暴露可变varJava 任意改写破坏不可变模型。companion里只有JvmStatic方法、无constJava 仍要类名前缀——与 Javastatic工具类习惯不一致时漏注解。动手练Acompanion object里fun parse(json: String)要给 Java 静态调用加哪条注解B起点下面 Java 调用编译失败补注解并说明原因objectRouteTable{fundeepLink(path:String,animated:Booleantrue):Uri{/* ... */}}// Java: RouteTable.deepLink(/home);第 3 章 SAM 转换与函数类型lambda 在 Java 侧的落点Kotlin 对函数式接口仅一个抽象方法的 interface自动做 SAM 转换lambda 可替代匿名类。Java 8 同样支持 lambda但 Kotlin 与 Java 在函数类型、内联 lambda上并不对称。Kotlin 定义 SAM推荐fun interfaceK2 下语义清晰funinterfaceOnItemClick{funonClick(id:String)}funsetListener(listener:OnItemClick){/* ... */}// Kotlin 调用setListener{id-navigate(id)}// Java 调用 —— 与 Kotlin lambda 同一 SAMsetListener(id-navigate(id));Java 定义 SAMKotlin 调用publicinterfaceRetryPolicy{booleanshouldRetry(intattempt,IOExceptione);}valpolicyRetryPolicy{attempt,e-attempt3e!isSSLException}函数类型() - Unit不是 SAM若 API 参数是 Kotlin 函数类型而非fun interfaceJava 侧不能写 lambda需Function0Unit或改成fun interface。RetrofitCallback、OkHttp 拦截器链是工程里最常见的 Java SAM 边界。Kotlin 侧用 lambda 写很爽但要意识到异常与 null 契约仍按 Java 规则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 APIKotlin 只是在边界内配置 Builder。若把Callback包一层 Kotlinsuspend应在 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 回调异步触发时读到过期值。对 JavaRunnable传{ }却期望返回值的映射逻辑——Unit与void混淆。动手练C定义fun interface Validator实现validate(input: String): Boolean并写 Kotlin / Java 各一行调用骨架。B起点将 Java 匿名类改为 Kotlin SAM lambdabutton.setOnClickListener(newView.OnClickListener(){OverridepublicvoidonClick(Viewv){submit();}});第 4 章 Nullable、NotNull 与跨语言 null 契约仅靠 Kotlin 的T?无法让 Java 调用方「看见」可空性需要在 JVM 字节码上携带JSR-305 / Jetpack 可空注解Kotlin 编译器才能把 Java API 读成T或T?而不是T!。常用注解Android 工程优先 Jetpack注解含义Kotlin 侧读取Nullable允许 nullT?NotNull禁止 nullT无注解未知平台类型T!Kotlin 暴露给 Javaimportandroidx.annotation.Nullableimportandroidx.annotation.NonNullclassUserBridge{NullablefunfindName(id:String):String?repository.find(id)?.nameNonNullfunrequireName(id:String):StringfindName(id)?:throwIllegalArgumentException(missing$id)}在 Kotlin 文件里为 Java 互操作设默认file:JvmName(OrderCompat)file:JvmDefaultWithoutCompatibilitypackagecom.example.orderJvmDefaultWithoutCompatibility影响接口默认实现与 Java 二进制兼容适用于库作者App 模块更常见的是对对外模块的 public API 补齐Nullable/NonNull。与 Kotlin 空安全协同从带Nullable的 Java 方法返回值赋给 Kotlinval x: String会编译报错迫使在边界处理funbindTitle(javaApi:LegacyApi){valtitle:StringjavaApi.getTitle()// 若 getTitle 标 Nullable应改为 String?// ...}团队规范建议新 Kotlin public API 一律显式T?/T供 Java 调用的模块同步加 Jetpack 注解遗留 Java 无注解 API 在 Adapter 层一次性收窄。原理补充Kotlin 编译器内置对 Android、org.jetbrains.annotations、javax.annotation等可空注解的识别表。混编时开启-Xjsr305strict或项目默认的 strict 模式可把无注解 Java 类型当更宽松平台类型具体以模块build.gradle.kts为准。踩坑Kotlin 返回String?却未标NullableJava 调用方静态分析工具仍当非空。混用javax.annotation与androidx.annotation部分工具链只认其一。在 Kotlin 内部用!!「帮 Java 兜底」把契约推迟到崩溃栈。动手练AJava 方法String getNote()实际可能返回 nullKotlin 侧应要求加哪个注解、读到什么类型D搜索工程里androidx.annotation.Nullable或 Java 源文件无注解的 getter列 1 处并给出 Kotlin 边界收窄写法。第 5 章 suspend 的 Java 视角Continuation 与调用边界suspend函数是 Kotlin 协程的核心但JVM 字节码上不是「多一个 async 关键字」而是带有隐藏Continuation参数的状态机。Java不能像 Kotlin 一样直接await loadUser()这是混编里最容易被误解的一点。Kotlin 编译后概念性suspendfunloadUser(id:String):User// 近似 JVM 签名// Object loadUser(String id, Continuation? super User continuation)因此Java 调用 Kotlinsuspend应使用kotlinx-coroutines-core提供的适配如FutureKt.future、BuildersKt等或在 Kotlin 侧提供非 suspend 包装给 Java。Kotlin 调用 Java普通阻塞 API 用withContext(Dispatchers.IO)勿假设 Java 回调能自动变suspend需suspendCancellableCoroutine手动挂起。给 Java 模块的推荐包装classUserRepository(privatevalapi:UserApi){suspendfunloadUser(id:String):Userapi.fetch(id).let(::mapUser)// Java 调用面显式阻塞或 Future不把 Continuation 泄漏出去JvmOverloadsfunloadUserBlocking(id:String,timeoutMs:Long5_000):User?runBlocking{withTimeout(timeoutMs){loadUser(id)}}}生产环境更推荐Kotlin 协程层完成异步Java 只接回调或ListenableFuturekotlinx-coroutines-guava避免在 UI 线程调用runBlocking。从 Java 回调到 suspendRepository 内收口suspendfunfetchLegacy():DatasuspendCancellableCoroutine{cont-legacyJavaClient.load(object:LegacyCallback{overridefunonSuccess(value:Data){if(cont.isActive)cont.resume(value)}overridefunonError(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 APIJava 侧无法完成。动手练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 各写擅长的层契约在接口上闭合。推荐分层Java 遗留 UI / 老 SDK 适配 ↓ 平台类型 / Nullable 收窄 Kotlin Domain Repositorysuspend、Flow、data class ↓ JvmStatic 工具、JvmOverloads 构造 对外 SDK / 供 Java 模块调用的 FacadeFacade 模式示例objectProvinceFacade{JvmStaticfunloadDisplayName(context:Context,provinceId:String,callback:OnNameLoaded,){valscopeCoroutineScope(SupervisorJob()Dispatchers.Main.immediate)scope.launch{valnamerunCatching{withContext(Dispatchers.IO){repository.load(provinceId).nameCn}}.getOrNull()callback.onLoaded(name)}}funinterfaceOnNameLoaded{funonLoaded(name:String?)}}Java 老页面只依赖ProvinceFacade与OnNameLoaded不碰suspend与StateFlow。反模式清单反模式问题改法平台类型穿透 UI随机 NPE映射层?:/let收窄处处runBlocking桥接阻塞与死锁Facade 主线程回调public API 用函数类型Java 无法 lambdafun interface无Nullable的 Java 返回Kotlin 当非空用补注解或 Adapter 校验!!处理 Java 边界契约失效显式T? 降级动手练B起点下面混编代码有三处边界问题列出并改写classBadBridge(privatevalbundle:Bundle){funtitle():Stringbundle.getString(title)!!// ①funload(cb:(String)-Unit){/* ... */}// ② Java 调不了suspendfunrefresh(){/* ... */}// ③ Java 直接调}D 验收清单选 1 个 Kotlinobject工具类确认 Java 模块能否静态调用若不能补JvmStatic并记录调用前后差异。面试速查 · 追问链追问链 #1什么是平台类型T! 标准回答≤200 字平台类型是 Kotlin 引用 Java或无 null 注解声明时的特殊类型对编译器而言可空也可非空写作T!。编译器不强制 null 检查运行时仍可能 NPE。应在 Repository/ViewModel 边界立刻收窄为T或T?不要穿透到 UI。追问 1SavedStateHandle.getString()在 Kotlin 里算什么答来自 Java/Android API 的平台类型边界应getString(...)?.let或赋给String?再处理避免!!。追问 2Kotlin 返回String?给 Java 要注意什么答同步加Nullable否则 Java 静态分析与 Kotlin 契约不一致。追问链 #2JvmStatic、JvmField、JvmOverloads 分别解决什么 标准回答≤200 字JvmStatic让companion/object成员在 Java 里是真正静态方法JvmField暴露 public 字段跳过 getterJvmOverloads为带默认参数的构造/方法生成 Java 重载。混编库对外工具类几乎必备JvmStatic。追问 1Kotlin 默认参数 Java 为什么调不了答JVM 只保留全参签名需JvmOverloads或 Java 侧手动传齐参数。追问 2const val需要JvmField吗答不需要const val已是编译期常量Java 直接用类名访问。追问链 #3Kotlin lambda 什么情况下 Java 不能用 ⭐标准回答≤200 字参数是 Kotlin 函数类型如(String) - Unit时Java 不能 SAM 转换需Function1等或改成fun interface。只有 Java 函数式接口或 Kotlinfun interface才支持 lambda 互调。内联高阶函数的 lambda 不宜存给 Java 字段。追问 1fun interface和普通interface对 Java 有何差别答fun interface明确单抽象方法Kotlin/Java 两侧都可 lambda多方法接口必须匿名类。追问 2RetrofitCallback在 Kotlin 里怎么写答可用 SAM lambda但应在 Repository 层封装避免 Java 模块依赖协程细节。追问链 #4Java 能直接调用 suspend 函数吗 ⭐标准回答≤200 字不能。suspend编译后带Continuation参数是 Kotlin 协程 CPS 形态。Java 应调包装方法runBlocking慎用、Future/ListenableFuture适配或回调/fun interfaceFacade。Kotlin 侧用suspendCancellableCoroutine把 Java 回调挂起仅在 Repository 内。追问 1为什么主线程runBlocking危险答阻塞 UI 线程且与已挂起的协程争用同一线程可能死锁。追问 2老 Java Activity 如何接 KotlinStateFlow答stateFlow.asLiveData()或在 Kotlin Facade 里转回调不让 Java 直接碰suspend。追问链 #5Nullable 和 KotlinT?怎么配合 标准回答≤200 字Java API 标Nullable/NotNull后Kotlin 编译器把返回类型读成T?/T减少平台类型。Kotlin 对外 public API 也应补 Jetpack 注解方便 Java 与静态分析。无注解的遗留 Java 返回视作T!在 Adapter 一次性校验。追问 1-Xjsr305strict有什么效果答对 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。相关推荐Kotlin 语法与空安全Android 开发第一课Kotlin 作用域函数let/apply 工程选型