7.2 混合项目迁移路线


7.2 混合项目迁移路线

本节摘要:存量 Java 工程迁 Kotlin 的正确姿势是渐进:一次一个文件、从外围到核心、每步有验收。本节给出按模块的迁移顺序(新代码、工具类、数据模型、UI、核心业务)、一键转换器的产出质量与手工精化要点、Kotlin API 对 Java 调用方的注解配合(JvmStatic、JvmOverloads、JvmField、JvmName)、迁移的验收清单与止损点。

为什么"一次性全转"是错的

IDE 提供了诱人的按钮:把 Java 文件一键转成 Kotlin。于是有些团队选了个周五晚上,全工程转换、修编译错误、提交。结果通常是一场灾难预演:五千个编译错误、自动转换产出的"Java 味 Kotlin"(满屏 !!Pairs.create 式的旧习惯)、git blame 全部指向同一个提交、下个月的线上崩溃都在排查"转换引入的回归"。一次性转换的问题不是工作量,是风险不可见——改动面太大,任何回归都无法归因。

渐进迁移的哲学相反:每一步小到可以归因,每一步都让工程比之前更安全。Kotlin 与 Java 的完全互操作(这是它相对其他 JVM 语言的核心优势)恰好支撑这种蚕食策略:同一个类里不能混两种语言,但同一个包、同一个模块里随便混,调用关系无缝。迁移不需要"切换日",只需要方向。

迁移顺序:从外围到核心

推荐五个阶段,顺序背后的逻辑是风险从低到高、收益从早到晚

阶段一:新代码一律 Kotlin。今天起,新增文件全部 Kotlin。零迁移风险(没有存量行为要保),立即建立团队熟悉度。这一步唯一要做的是骨架决策(第 1 章):kts、viewBinding、协程依赖一次配齐。

阶段二:工具类与扩展化改造。第 6 章的主场。把 StringUtils、DensityUtils 这类无状态、高复用、测试薄的工具类转成 Kotlin 并顺手扩展化——不是翻译(Utils.f(x) 原样保留),是归位(x.f())。风险低(纯函数好验证),收益立竿见影(新代码从此用上补全直达的扩展)。

// 转换前 DensityUtils.dp2px(context, 16) // 转换后 归位为扩展 val Int.dp: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()

阶段三:数据模型。JavaBean 转 data class。模型类字段多、样板重,是转换收益密度最高的地方——六行 JavaBean 换一行 data class,equals 与 hashCode 的手写风险(第 3 章开头的幽灵 bug)顺带消灭。模型被全工程引用,转换后编译器会把所有调用点的 getter 调用(getName())识别为属性访问(name),不需要手工改调用方。

阶段四:UI 层。Activity、Fragment、Adapter。引入 viewBinding、把回调改协程(第 5 章)。UI 层改动面大但验证容易——页面行为肉眼可见,配合第 8 章的 UI 测试可以自动化验收。

阶段五:核心业务,或者停在这里。核心业务逻辑迁移的风险最高(行为复杂、测试覆盖往往不足),收益也最模糊(业务代码 Java 与 Kotlin 都能写对)。止损点是合法决策:如果团队评估阶段五的回归风险大于收益,停在阶段四是完全合理的状态——一半 Kotlin 一半 Java 的工程只要边界纪律(7.1 节)在,就是健康状态。迁移是手段,不是政绩。

一键转换的产出质量

转换器(IDE 的 Java 转 Kotlin 动作)值得信任到什么程度?它语法正确率接近满分,地道程度接近零。看它的典型产出:

// 转换器产出 能跑 但到处是坏味道 class OrderManager { private var orders: MutableList<Order>? = null // Java 字段默认 null 转成可空 fun getOrder(id: String?): Order? { // 参数全变可空 if (orders == null) { // 人肉判空回归 orders = ArrayList() } for (o in orders!!) { // 双感叹号登场 if (o.id == id) { return o } } return null } }

手工精化要做的事,恰好是前六章的全部内容:可空性重新判决(字段实际语义是"稍后初始化"就该 lateinit,"可能没有"才是问号);!! 替换为 Elvis 或 let;for 循环换 firstOrNull;判空初始化换 lazy。精化后的等价版本:

class OrderManager { private val orders = mutableListOf<Order>() fun getOrder(id: String): Order? = orders.firstOrNull { it.id == id } }

转换器的角色是打字员,不是工程师——它把代码搬到 Kotlin 的纸上,字迹仍然是 Java 的。团队要预留的工时是"转换加精化",只算转换的部分必然返工。精化清单可以直接抄各章要点:第 2 章判可空、第 3 章上数据类、第 4 章收可变面、第 6 章扩展化。

Kotlin API 对 Java 调用方的注解配合

迁移是双向街:Kotlin 新代码也会被 Java 老代码调用。四个注解让 Kotlin API 对 Java 显得自然,它们的缺失是"Java 调 Kotlin 很别扭"的根源:

class TimeKit { companion object { @JvmStatic // Java 侧 TimeKit.format 而非 TimeKit.Companion.format fun format(ms: Long): String = "%d:%02d".format(ms / 60000, ms % 60000 / 1000) } } @JvmOverloads // 生成三个重载 Java 侧可任选参数组合 fun showBanner(text: String, duration: Long = 3000, canDismiss: Boolean = true) { } class Config { @JvmField // Java 侧直接 config.debug 而非 config.getDebug var debug = false } @JvmName("argCount") // 解决与既有 Java 方法签名冲突 fun count(args: List<String>): Int = args.size

规律很好记:JvmStatic 让静态像静态,JvmOverloads 让默认参数变成重载,JvmField 让属性像字段,JvmName 解决签名冲突。纯 Kotlin 调用方完全看不见这些注解,它们只为 Java 侧服务——迁移期的"双向礼貌"。等 Java 调用方陆续迁完,这些注解可以随宿主一起删除。

另一个方向的小坑:Kotlin 的伴生对象里的 const val 对 Java 是真正的编译期常量(内联进调用方),普通 val 则是运行时读取——把不变常量标成 const,对两个语言侧都是优化。

验收清单与止损点

每个阶段退出的验收标准,建议做成 checklist 硬执行:

  1. 编译与静态检查全绿:Detekt 与 lint 对新转文件零严重告警(第 8 章配置);
  2. 测试覆盖不下降:转换前先补测试再转换——"有测试的代码才配被迁移",没有测试的核心业务属于阶段五高风险区,要么先补测试要么不迁;
  3. 平台类型出清:新 Kotlin 文件里 grep 不到"感叹号类型"的遗留使用(显式声明或注解覆盖);
  4. !! 数量为零或有注释:转换器爱塞 !!,精化的硬指标是清零或注释化;
  5. 行为回归归因可行:本阶段的改动单独成批提交,出问题能二分定位。

止损点的判断标准回到成本收益:当某个模块的行为复杂度、测试覆盖率、人员熟悉度三者任何一个不支持安全迁移时,停在原地。工程里最贵的不是没迁完,是迁坏了没法归因

渐进迁移路线全景

迁移期的团队协作与评审清单

技术路线之外,迁移成败常取决于协作节奏。三个实践建议。其一,转换责任落在提交者:谁改哪个文件、谁负责该文件的转换与精化——集中式"迁移专项"会与业务分支冲突不断,随改动顺带转换最顺。其二,评审清单加四问:这个文件转换后 !! 清零了吗;平台类型出清了吗;可空的语义是真可空还是偷懒(问号是判决不是避难所);Java 调用方有没有因此需要 Jvm 注解。其三,双语言共存期的文档约定:新功能入口文档标注语言(避免"以为这边是 Java 生态"的依赖误判),招聘与培训材料同步更新——迁移也是认知工程,代码只走了一半。

进度度量也要有仪表:Kotlin 文件占比(粗粒度趋势)、新增代码的语言比例(方向性指标,低于九成要查原因)、!! 密度与平台类型引用数(质量指标,随精化下降)。三个数字每季度看一次就够——迁移是长跑,仪表盘只在方向跑偏时需要。

常见问题

问:团队里有人坚决反对迁移,怎么办?
先听技术理由再谈立场。"转换引入回归"用阶段验收清单回应(测试覆盖不下降才迁);"两套语言维护成本高"用互操作事实回应(混编零成本,工具链一体);"学习成本"用阶段一的渐进回应(新代码开始、无存量风险)。如果技术理由都站不住、剩下的是偏好,用数据说话:崩溃率的边界 NPE 占比、样板代码的评审耗时、新人的上手速度。真正的共识出现在第一个迁移完成的模块稳定运行一个月之后——让结果去说服,比会议辩论有效。给反对者留一个体面的参与方式:让他负责验收清单的制定,把关者角色往往比被推动者角色更容易转变立场。

问:转换一个文件后编译通过、测试全绿,可以直接提交吗?
还差两步。静态检查(Detekt)对新文件跑一遍——转换器产出的坏味道(可变集合滥用、过宽 catch)测试发现不了;人工通读一遍 diff,重点看可空性判决与 !!——测试覆盖的是行为等价,判决质量要靠眼睛。两步都过再提交,并把转换与业务改动分开提交——混在一个提交里,将来 bisect 会痛不欲生。

问:什么时候应该正式宣布"迁移完成"?
宣布本身有害无益——它暗示存在终点线,而 Java 生态的新依赖(新 SDK、新库)随时会把 Java 代码重新带进工程。健康的表述是维持一个常态:新代码 Kotlin、存量按需转换、边界纪律永远在线。真正值得宣布的是里程碑("数据层全 Kotlin 了""UI 层全 Kotlin 了"),它们对应阶段验收,而不是终点仪式。把迁移当状态而不是事件,是长期混合工程的正确心态。

本节要点回顾

  • 渐进优于一次到位:每步小到可归因、让工程更安全;互操作性支撑蚕食,不需要切换日;
  • 五阶段顺序:新代码、工具类扩展化、数据模型、UI 层、核心业务——风险从低到高,止损点合法;
  • 转换器是打字员:语法对、不地道,精化工时必须预留,清单就是前六章的要点;
  • 四个 Jvm 注解:静态、重载、字段、改名——Kotlin 对 Java 调用方的双向礼貌;
  • const val 是真常量:编译期内联,对两侧都是优化;
  • 验收五条:编译绿、测试不降、平台类型出清、双感叹清零、回归可归因。

边界的工事变工事讲完了。最后一章把五根支柱接进真实的安卓工程:现代 UI 层、ViewModel 数据流、性能防线与静态检查,组成一套完整的安全消除体系。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U