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 中 @Deprecatedmessage 参数是必填项,不像 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)会识别这一信息,提供一键替换建议:

Replace With

处理导入依赖

如果替换表达式涉及未导入的类,需显式声明 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 属性显式指定

✅ 最佳实践:对于跨包迁移,务必带上 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


原始标题:Deprecation in Kotlin