1. 概述
如果你有 Java 背景,应该对 @Deprecated
注解不陌生——它是 JVM 生态中标识过时 API 的标准方式。
但在 Kotlin 中,弃用(Deprecation)机制远不止打个标记那么简单。它提供了更细粒度的控制、更强的工具支持以及更清晰的迁移路径。本文将带你深入理解 Kotlin 如何把“废弃”这件事做到极致,避免你在实际开发中踩坑。
2. 基础弃用:从一个日期类说起
提到 JVM 中的经典“反面教材”,java.util.Date
及其一堆被弃用的方法绝对榜上有名。我们不妨在 Kotlin 中复刻一个简化版来演示:
data class Date(val millisSinceEpoch: Long) {
private val internal = LocalDateTime.ofInstant(Instant.ofEpochMilli(millisSinceEpoch), ZoneId.of("UTC"))
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)
}
这个 Date
类封装了时间信息,monthNumber()
返回月份数字(1~12):
val epoch = Date(0)
println(epoch.monthNumber()) // 输出 1
为了避免“零基 vs 一基”的常见混淆,我们引入一个返回枚举的新方法:
fun month(): Month = internal.month
此时,旧方法就成了遗留代码。为了让调用者知道该用新方法,我们可以使用 @Deprecated
注解:
@Deprecated("Use the new month() method")
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)
⚠️ 注意:Kotlin 中 @Deprecated
的 message
参数是必填项,不像 Java 可以省略。
一旦使用该方法,编译器就会发出警告:
'monthNumber(): Int' is deprecated. Use the new month() method
编译器不仅显示你写的提示信息,还会自动拼接上符号签名,非常贴心。
✅ 小贴士:良好的弃用消息应包含“为什么弃用”和“该用什么替代”。
3. 弃用级别:控制严格程度
默认情况下,使用被弃用的方法只会产生警告,不影响编译通过。但 Kotlin 允许你通过 level
参数调整行为,实现更严格的管控。
3.1 DeprecationLevel.WARNING(默认)
仅警告,编译继续:
@Deprecated("Use month()", level = DeprecationLevel.WARNING)
fun monthNumber(): Int = ...
适合渐进式迁移,给团队缓冲期。
3.2 DeprecationLevel.ERROR
直接让编译失败:
@Deprecated("Use month()", level = DeprecationLevel.ERROR)
fun monthNumber(): Int = ...
错误信息:
Using 'monthNumber(): Int' is an error. Use the new month() method
✅ 适用场景:
- 已进入最后清理阶段
- 旧接口存在严重安全或性能问题
- 团队已达成共识,不允许再使用
3.3 DeprecationLevel.HIDDEN
最狠的一招——让方法在编译期“消失”:
@Deprecated("Use month()", level = DeprecationLevel.HIDDEN)
fun monthNumber(): Int = ...
结果:
Unresolved reference: monthNumber
⚠️ 关键点:即使方法被 HIDDEN,它依然存在于字节码中!
验证一下生成的 bytecode:
$ javap -c -p -v com.example.Date
// 省略部分输出
public final int monthNumber();
descriptor: ()I
flags: (0x1011) ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC
Code:
// ... 字节码逻辑仍然存在
Deprecated: true
这意味着:
- ✅ 二进制兼容性保留:老版本 jar 包仍可正常调用
- ❌ 源码不可见:新代码无法引用,强制使用新 API
这种设计非常巧妙,既保证了平滑升级,又防止新代码继续依赖旧接口。
4. 自动替换:提升开发体验
Kotlin 不止告诉你“别用了”,还能告诉你“该用啥”,甚至帮你自动替换!
通过 replaceWith
参数,你可以指定替换方案:
@Deprecated(
"Use the new month() method",
replaceWith = ReplaceWith("month()")
)
fun monthNumber(): Int = internal.get(ChronoField.MONTH_OF_YEAR)
现代 IDE(如 IntelliJ IDEA)会识别这一信息,提供一键替换建议:
处理导入依赖
如果替换表达式涉及未导入的类,需显式声明 imports:
companion object {
@Deprecated(
"Use java.time instead",
replaceWith = ReplaceWith("LocalDateTime.now()", imports = ["java.time.LocalDateTime"])
)
fun now(): Date = Date(System.currentTimeMillis())
}
这样,IDE 在替换时会自动添加所需 import:
📌 原理说明:
- 替换表达式在当前作用域内解析
- 但不会继承原文件的 imports → 必须通过
imports
属性显式指定
✅ 最佳实践:对于跨包迁移,务必带上 imports,否则自动修复功能会失效。
5. 多样化的弃用目标
@Deprecated
不只适用于方法!查看其定义即可知支持范围:
@Target(
CLASS,
FUNCTION,
PROPERTY,
ANNOTATION_CLASS,
CONSTRUCTOR,
PROPERTY_SETTER,
PROPERTY_GETTER,
TYPEALIAS
)
public annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""),
val level: DeprecationLevel = DeprecationLevel.WARNING
)
支持的弃用目标包括:
目标 | 示例 |
---|---|
CLASS |
整个类过时 |
PROPERTY |
成员属性 |
TYPEALIAS |
类型别名 |
CONSTRUCTOR |
特定构造函数 |
PROPERTY_GETTER/SETTER |
单独弃用 getter/setter |
实际用例
// 弃用属性
@Deprecated("不再需要此字段")
private val legacyFlag: Boolean = false
// 弃用类型别名
@Deprecated("直接使用 String")
typealias JsonString = String
// 弃用构造函数
class UserService @Deprecated("Use dependency injection") constructor()
这种灵活性让你可以在任何粒度上管理 API 演进。
6. 总结
Kotlin 的弃用机制远超 Java 的简单标记,核心优势体现在:
✅ 多级控制:WARNING / ERROR / HIDDEN,适配不同迁移阶段
✅ 智能迁移:replaceWith + imports 支持 IDE 自动重构
✅ 二进制兼容:HIDDEN 级别不影响运行时调用
✅ 广泛适用:覆盖类、属性、构造器等几乎所有语言结构
合理利用这些特性,能显著提升 API 设计质量,降低维护成本。尤其在构建 SDK 或公共库时,优雅的废弃策略是专业性的体现。
示例代码已上传至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-6