第 1 章 为什么互操作是 Android 日常

即便新模块用 Kotlin 写,你仍会碰到:

  1. 老 Java 库 / SDK:返回类型在 Java 里没标 @Nullable
  2. 团队遗留 Java 模块:Kotlin 调 Utils,Java Activity 调 Kotlin 扩展。
  3. Android 框架 APIBundleIntent 在 Java 侧声明常不带精确可空。

工程原则:在边界收口,不在 UI 深处赌运气。从 Java 进来的值,在映射层立刻变成 TT?;向外暴露给 Java 的 Kotlin API,用注解把契约写进字节码。


第 2 章 平台类型:T! 是什么

Kotlin 引用 Java(或无 null 注解)声明时,得到平台类型,写作 T!——编译器不强制 null 检查,运行时仍可能 NPE。

// Java: String getTitle()  // 无 @Nullable
val title: String = javaBean.title   // 平台类型,可当非空用——有风险
val safe: String? = javaBean.title   // 更稳妥:主动当可空

纯 Kotlin 互操作示例(模拟 Java 边界):

// 模拟 Java 返回未标注可空的 String
fun fakeJavaGetName(): String! = "阿 Kotlin"

fun greet(): String {
    val name = fakeJavaGetName()  // 平台类型
    return name.uppercase()       // 若 Java 实际返回 null → NPE
}

Android 对照intent.getStringExtra("city") 在 Kotlin 里是 String?,这是已标注的 API;但若调自家 Java 工具类无注解,就会遇到平台类型。

踩坑

  • 把平台类型一路传到 Composable / Adapter,最深处的 !! 崩溃最难查。

第 3 章 可空注解与 Kotlin 识别

在 Java 侧使用 JetBrains / AndroidX 注解,Kotlin 能识别:

// Java
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

public class UserParser {
    @Nullable
    public String parseName(@NotNull String raw) { ... }
}
val name: String? = UserParser().parseName("x")  // 正确推断为 String?

Kotlin 调 Java 集合时,注意可变性与只读映射:java.util.List 在 Kotlin 里是 MutableList 平台视图。


第 4 章 从 Kotlin 暴露给 Java:@Jvm*

Java 看不到 Kotlin 的默认参数、顶层函数、object 单例,需要注解:

class OrderFormatter {
    @JvmOverloads
    fun format(price: Double, currency: String = "CNY"): String =
        "$price $currency"
}

object AppConfig {
    @JvmStatic
    fun apiBaseUrl(): String = "https://api.example.com"
}
  • @JvmOverloads:为默认参数生成 Java 重载。
  • @JvmStatic:在 object / companion 里生成真正静态方法。
  • @JvmName:解决泛型擦除导致的签名冲突(进阶)。

顶层函数编译为 FileNameKt 的静态方法,Java 调用:DemoKt.greet()

踩坑

  • Kotlin data classcomponentN() 给 Java 用较别扭;给 Java 消费的 DTO 有时保留 Java Bean 或加 @JvmField

第 5 章 SAM 与 lambda:Java 接口怎么接

Java 单抽象方法接口(SAM)在 Kotlin 可用 lambda:

button.setOnClickListener { view ->
    view.isEnabled = false
}

Kotlin 函数类型与 Java Function 互转时,注意 Unitvoid() -> Unit 在 Java 侧有时要 Function0<Unit> 或显式 return


第 6 章 Android 边界:Bundle、Intent 与收窄

标准模式:读参数 → 立刻收窄 → 再往下传非空类型(片段示例):

fun readCity(intent: Intent): String =
    intent.getStringExtra("city")?.trim().takeUnless { it.isNullOrEmpty() }
        ?: "未知城市"

fun bindGreeting(textView: android.widget.TextView, intent: Intent) {
    textView.text = "你好,${readCity(intent)}"
}

从 Java HashMap 或 JSON 库进来的值,在进入领域模型前完成 ?.let / ?: / requireNotNull(仅确定非空时)处理。

踩坑

  • 在 UI 层对平台类型用 !!「反正不会 null」——混合工程里这是 NPE 温床。

动手练

练习 1:平台类型(A · 基础)

目标:理解 T!

要求:说明平台类型为何「编译器不强制检查 null」;写一行把平台类型赋给 String? 的代码。

练习 2:@JvmOverloads(A · 基础)

目标:Java 可调默认参数。

要求:为 fun connect(host: String, port: Int = 443) 加注解,并说明 Java 能怎么调。

练习 3:@JvmStatic(D · Android 实践)

目标:object 给 Java 用。

要求object Logger { @JvmStatic fun d(msg: String) },写出 Java 调用语句 Logger.d("ok");

练习 4:坏代码收窄(B · 进阶)

起点

fun showTitle(javaApi: JavaTitleApi, view: android.widget.TextView) {
    view.text = javaApi.getTitle()!!.uppercase()
}

要求:去掉 !!,null 时显示 "无标题"JavaTitleApi.getTitle() 为平台类型 String!)。

练习 5:SAM(A · 基础)

目标:lambda 接 Java 接口。

要求:用 lambda 写 list.sortWith { a, b -> a.compareTo(b) } 的等价比较器(纯 Kotlin List<String> 即可)。

练习 6:边界收口(C · 综合)

目标:Intent 参数链。

要求fun parseCount(intent: Intent): Int,从 getStringExtra("count") 解析,非法或缺失返回 0


进阶阅读(可选)

追问链 #1:平台类型在防什么? 🔥

参考回答:Java 无统一可空注解时,Kotlin 无法知道能否为 null,故给 T!。应在边界收窄为 T/T?,不要穿透 UI。

追问链 #2:@JvmOverloads 生成几个重载? 🔥

参考回答:为每一组「从右向左省略默认参数」的组合生成 Java 重载(受 JVM 限制,极多默认参数时可能不全)。

追问链 #3:Kotlin object 在 Java 里长什么样? ⭐

参考回答:单例实例 INSTANCE@JvmStatic 成员才是静态方法。

追问链 #4:为什么 getStringExtra 是 String? 而不是平台类型? 💡

参考回答:AndroidX/Kotlin 增强声明或已标注可空;自家 Java 代码若无注解仍是平台类型。


完整链路一句通

Java 无注解 → 平台类型 T!边界立刻收窄为 T/T?向外暴露用 @JvmStatic/@JvmOverloadsBundle/Intent 用 ?./?: 再进 UI


请添加图片描述

相关推荐

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐