Skip to content

JEP 277: Enhanced Deprecation | 增强的弃用功能

摘要

改进 @Deprecated 注解,并提供工具来加强 API 生命周期。

目标

  • 提供更好的关于规范中 API 状态和预期处置的信息。
  • 提供一个分析应用程序静态使用过时 API 的工具。

非目标

本项目的目标不是将 @deprecated Javadoc 标签与 @Deprecated 注释统一起来。

动机

过时是一种传达 API 生命周期信息的技术:鼓励应用程序迁移到其他 API,阻止应用程序依赖该 API 并告知开发人员继续依赖该 API 的风险。

Java 提供了两种表达过时的机制:@deprecated Javadoc 标记,引入自 JDK 1.1,以及 @Deprecated 注解,引入自 Java SE 5。 @Deprecated 注解的 API 规范,反映在 Java 语言规范中,是:

被注释为 @Deprecated 的程序元素是程序员被建议不使用的元素,通常因为它是危险的,或者存在更好的替代方案。当在非过时代码中使用或覆盖过时的程序元素时,编译器会发出警告。

然而,@Deprecated 注解最终被用于几种不同的用途。实际上很少有过时的 API 被删除,这导致一些人认为永远不会删除任何东西。另一方面,其他人认为所有被过时的东西最终都可能会被删除,这也不是本意。(虽然在规范中没有明确说明,但各种文档提到过时的 API 将在某个时候被删除。) 这导致开发者对 @Deprecated 的含义和遇到已过时 API 使用时该做什么感到困惑。每个人都对过时的含义感到困惑,没有人认真对待它。反过来,这使得从 Java SE API 中删除任何内容变得困难。

过时的另一个问题是警告仅在编译时发出。随着 API 在 Java SE 的连续版本中被过时,现有的二进制文件继续依赖和使用过时的 API 而没有警告。如果在 JDK 发行版中删除一个已过时的 API,即使在一个或多个版本被过时之后,这将给旧应用程序二进制文件的用户带来不愉快的惊喜。该应用程序将突然失败并出现链接错误,而没有发出任何警告。更糟糕的是,开发人员没有办法检查现有的二进制文件是否有任何对过时的 API 的依赖性。这在运行旧二进制文件于新 JDK 发布之间进行权衡时造成了显着的紧张,而需要通过撤退旧 API 来推进规范的演变。

总之,Java SE API 中的过时机制在应用上被应用不一致,导致了对原则上过时含义和实践中正确使用过时的困惑。

描述

规范

增强 @Deprecated 注解的主要目的是向工具提供有关 API 过时状态的更精细信息。这些工具进而使用该注解向 API 的用户报告信息。@Deprecated 注解具有运行时保留,因此会消耗堆内存。因此,这里的信息应该是最小和明确定义的。

要添加到 java.lang.Deprecated 注解类型的元素如下:

  • 一个返回 boolean 的方法 forRemoval()。如果为 true,则表示此 API 元素在将来的版本中标记为删除。如果为 false,则 API 元素已过时,但目前没有打算在将来的版本中删除它。该元素的默认值为 false

  • 一个名为 since() 的方法,返回 String。这个字符串应该包含此 API 变为过时的发布或版本号。它具有自由格式的语法,但发布编号应遵循包含过时 API 的项目的 @since Javadoc 标签相同的方案。请注意,此值与 Javadoc @since 标记不冲突,因为后者记录引入 API 的发布,而 @Deprecated 注解中的 since() 方法记录了 API 被弃用的发布。该元素的默认值为空字符串。

由于这些元素被添加到现有的 @Deprecated 注解中,如果注解处理程序处理的是使用早于 JDK 9 的 @Deprecated 版本编译的类文件,则会看到 forRemoval()since() 的默认值。

API 上的 @Deprecated 注解是 API 的作者或维护者向 API 的用户传达的信息。最常见的情况是,过时是建议用户将其使用迁移到过时 API,避免在新代码中或在维护旧代码时对此 API 添加依赖性,或是维护依赖于此 API 的代码存在一定风险的建议。有很多原因建议进行这种迁移。原因可能包括:

  • API 存在缺陷,不可行修复,

  • 使用 API 可能导致错误,

  • API 已被另一个 API 取代,

  • API 已过时,

  • API 是实验性的,可能发生不兼容的更改,

  • 或以上原因的任意组合。

废弃 API 的确切原因通常太微妙而无法表示为注解中的标志或元素值。强烈建议在 API 的文档注释中描述废弃 API 的原因。此外,还建议从文档中讨论并链接潜在的替代 API。

然而,提供了一个特定的标志值。如果 forRemoval() 布尔元素为 true,则表示有意在项目的某个未来版本中删除 API 元素。因此,API 的用户提前得到警告,如果他们不迁移到其他 API,则在升级到新版本时可能会导致代码中断。如果 forRemoval()false,则表示建议迁移离过时 API,但没有具体删除该 API 的意图。

@Deprecated 注解和 @deprecated javadoc 标记在 API 元素上应该同时存在或同时不存在。单独存在一个而没有另一个被视为错误。如果在缺乏 @Deprecated 注解的 API 上存在 @deprecated 标记,则 javac lint 标志 -Xlint:dep-ann 将发出警告。目前,如果反过来是真的,则不会发出警告;请参阅 JDK-8141234

@Deprecated 注解对废弃的 API 的行为不应产生直接影响,并且性能影响应该可以忽略不计。

在 Java SE 中的用法

@Deprecated 注解类型出现在 Java SE 中,因此可以应用于使用 Java SE 平台的任何类库的 API。这些类库使用 @Deprecated 注解类型的确切规则和政策是由这些库的维护者确定的。建议类库维护者制定和记录这样的政策。

本节描述了在 Java SE API 本身上使用 @Deprecated 注解类型以及管理此类使用的策略。

一些 Java SE API 将添加、更新或删除 @Deprecated 注解。Java SE 9 中实现的更改如下所示。除非另有说明,否则此处列出的弃用不会被删除。请注意,这不是 Java SE 9 中所有弃用的全面列表。

  • 为装箱基元类型 (BooleanInteger 等) 的构造函数添加 @Deprecated 注解 (JDK-8145468)

  • Runtime.traceInstructionsRuntime.traceMethodCalls 方法添加 @Deprecated(forRemoval=true) 注解 (JDK-8153330)

  • 为各种 java.applet 和相关类添加 @Deprecated 注解 (JEP 289)

  • java.util.ObservableObserver 添加 @Deprecated 注解 (JDK-8154801)

  • 为各种已经过时的安全 API(包括 java.security.acljavax.security.certcom.sun.net.ssljava.security.Certificate 以及 javax.security.auth.Policy 等)添加 @Deprecated(forRemoval=true) 注解 (JDK-8157847, JDK-8157712, JDK-8157707, 和 JDK-8157848)

  • java.lang.Compiler 添加 @Deprecated(forRemoval=true) 注解 (JDK-4285505)

  • 将多个 Java EE 模块和 java.corba 模块添加 @Deprecated 注解 (JDK-8169069,JDK-8181195,JDK-8181702,JDK-8174728)

  • 修改 Thread.destroy()Thread.stop(Throwable)Thread.countStackFrames()System.runFinalizersOnExit() 以及各种不再使用的 RuntimeSecurityManager 方法,以具有 @Deprecated(forRemoval=true) 注解 (JDK-8145468)

鉴于 Java SE 中弃用的历史以及跨版本的长期 API 兼容性的重点,删除 API 是一个非常关键的问题。因此,仅当在 Java SE 平台的下一个版本中有明确和确定的计划删除该 API 时,才应用带有元素 forRemoval=true 的弃用。

除非在先前的 Java SE 版本中提供了 @Deprecated(forRemoval=true) 注释,否则不应从 Java SE 规范中删除 API 元素。可以引入 forRemoval=true 的弃用。不需要先将其弃用为 forRemoval=false,然后升级到 forRemoval=true,再删除 API。

对于在 Java SE 9 及以上版本中弃用的 API 元素,since 元素应包含表示该 API 元素被弃用的 Java SE 版本字符串。版本字符串应符合 JEP 223 中指定的格式。由于 Java SE 通常仅在主要版本中进行规范更改,因此版本字符串通常仅包含“MAJOR”版本号。因此,对于在 Java SE 9 中弃用的 API 元素,since 元素值应简单地为“9”。

在 Java SE 9 之前已被弃用的 API 元素将会在时间允许的情况下填充其 since 值。 (对所有 API 执行此操作的价值有限,主要是历史研究的练习。) 在这种情况下,用于 since 值的字符串应符合用于这些发行版的 @since javadoc 标记的 JDK 版本约定,通常为 1.01.8,但有时带有“微”的发布号,例如 1.0.2。在 Java SE API 上查找此值并找到空字符串的注释处理工具应假定该弃用发生在 Java SE 8 或更早版本中。

弃用 API 将增加项目在构建新版本的 Java SE 时遇到的强制警告数。一些项目,包括 JDK 本身,使用启用详细警告和将警告转换为错误的编译器选项进行构建。对于这样的项目,将弃用的 API 添加到 Java SE 中可能会引入大量警告,并显著增加迁移到新版本的 Java SE 的工作量。已有的管理警告的机制,如 @SuppressWarnings 注解和编译器命令行选项,无法处理此问题。这实际上限制了哪些 API 可以在给定的 Java SE 版本中弃用,并使过时但流行的 API 的弃用几乎不可能。这需要未来的努力来增强可用于管理弃用警告的机制。

forRemoval 对警告策略的影响

Java 语言规范,第 9.6.4.6 节 规定了与被依赖 API("声明位置")的弃用状态以及使用该 API 的代码("使用位置")的弃用状态有关的特定警告行为。添加 forRemoval 元素会增加另一组需要定义的情况。为简洁起见,我们将 forRemoval=false 的弃用称为 "普通弃用",将 forRemoval=true 的弃用称为 "终止弃用"。

在 Java SE 8 及更早版本中,不存在 forRemoval,因此唯一一种弃用类型是普通弃用。是否发出弃用警告取决于使用位置和声明位置的弃用状态。下表列出了在 Java SE 8 中存在的情况:

java
    use site     | API declaration site
    context      | not dep.   deprecated
                 +-----------------------
    not dep.     |    N          W
                 |
    deprecated   |    N          N (1)

        N = no warning
        W = warning

(注 1) 这是一个奇怪的情况。如果使用位置和声明位置都被弃用,不会发出警告。如果两个位置都在一个单独的类库内,并作为一个单元进行维护和发布,这是有道理的。由于它们共同维护,对于这种情况不发出警告没有意义。然而,如果使用位置在与声明位置分开维护的类库中,它们可能以不同的速度演变,因此在这种情况下不发出警告可能是一个缺陷。然而,在 Java SE 5 引入 @SuppressWarnings 注解之前,这种机制对于减少 JDK 编译警告数量非常有用。

(JLS 9.6.4.6 还要求如果使用位置位于 相同的最外层类 内,则不发出警告。在这种情况下,使用位置和声明位置被定义为一起维护,因此不发出警告是合理的。)

在 Java SE 9 中,引入 forRemoval 会增加几个与终止弃用有关的新情况。这要求引入一种新类型的警告。

在普通弃用 API 的使用位置发出的警告是 "普通弃用警告",与 Java SE 8 及更早版本中的情况相同。出于惯例,这些警告通常被称为 "弃用警告"。

在终止弃用 API 的使用位置发出的警告可能会被称为 "终止弃用警告",但这样说起来过于冗长。因此,我们将这些警告称为 "移除警告"。

以下是所提出的情况表:

java
    use site     |      API declaration site
    context      | not dep.   ord. dep.   term. dep.
                 +----------------------------------
    not dep.     |    N         oW (2)       rW (5)
                 |
    ord. dep.    |    N          N (3)       rW (6)
                 |
    term. dep.   |    N          N (4)       rW (7)

(注 2) "oW" 指的是 "普通弃用警告",与 Java SE 8 及更早版本中的情况相同。

(注 3) 左上角的四个元素与 Java SE 8 表中的情况相同,出于向后兼容性的原因。

(注 4) 根据兼容行为,不发出警告。如果使用位置和声明位置都是普通弃用,那么如果将使用位置更改为终止弃用,发出警告将是错误的。因此,在这种情况下不发出警告。

(注 5) "rW" 指的是 "移除警告"。在终止弃用 API 的使用位置发出的所有警告都是移除警告。

(注 6) 这种情况非常重要。我们希望始终对终止弃用 API 的使用产生移除警告,即使使用位置在弃用代码内部。

(注 7) 这类似于第 6 种情况。人们可能会认为,由于使用位置和声明位置都是终止弃用,它们都将 "消失",因此在这里发出警告是没有意义的。但可能的情况是,声明位置位于比使用位置演化得更快的库中,因此使用位置可能会存活得比声明位置更久。因此,有必要对声明位置即将移除发出警告。

涵盖右下角四个元素的一般规则如下:如果使用位置被弃用,无论是普通弃用还是终止弃用,都不会发出普通弃用警告,但仍会发出移除警告。

普通弃用警告的示例可能如下所示:

java
UseSite.java:3: warning: [deprecation] ordinary() in DeclSite has been deprecated

移除警告的示例可能如下所示:

java
UseSite.java:4: warning: [removal] removal() in DeclSite has been deprecated and marked for removal

警告的具体措辞以及警告定制的机制可能因编译器而异。

抑制废弃警告

在 Java SE 8 及更早版本中,可以通过在使用位置上添加 @SuppressWarnings("deprecation") 注解来抑制废弃警告。但在存在终止废弃时,需要对此行为进行修改。

考虑这样一种情况:使用位置依赖于通常被废弃的 API,并且使用 @SuppressWarnings("deprecation") 注解已经抑制了生成的警告。如果声明位置被修改为终止废弃,我们希望在使用位置发出删除警告,即使使用位置上的警告已经被抑制了。如果在这种情况下不发出新警告,那么 API 可能会被终止废弃然后在使用位置没有任何警告的情况下被删除。

以下场景说明了这个问题。假设 @SuppressWarnings("deprecation") 注解不仅抑制普通废弃警告,还抑制删除警告。那么可能发生以下情况:

  1. 使用位置 X 依赖于尚未废弃的 API Y
  2. Y 的声明更改为普通废弃,在 X 上生成普通废弃警告
  3. X 被注解为 @SuppressWarnings("deprecation"),抑制警告
  4. Y 的声明更改为终止废弃,但 X 上的删除警告仍被抑制
  5. Y 被完全删除,导致 X 出现意外故障

鉴于废弃的目的是传达有关 API 演变的信息,特别是有关 API 删除的信息,这种情况下缺乏任何警告是一个严重的问题。因此,当将废弃从普通废弃升级为终止废弃时,即使先前在使用位置上的警告已被抑制,也应发出警告。

我们需要一种机制来抑制删除警告,该机制与当前用于抑制普通废弃警告的机制不同。解决方案是在 @SuppressWarnings 注解中使用不同的字符串。

可以使用以下注解来抑制由于使用终止废弃的 API 而产生的删除警告:

java
@SuppressWarnings("removal")

该注解只抑制删除警告,而不是普通废弃警告。我们考虑过使其成为一种强形式的抑制,既可以涵盖普通废弃警告,也可以涵盖删除警告。然而,这可能会导致错误。程序员可能使用 @SuppressWarnings("removal") 来抑制普通废弃的警告。这将阻止警告出现,如果将普通废弃更改为终止废弃,则会导致终止废弃的 API 最终被删除时出现意外故障。

与以前一样,可以使用以下注解抑制对通常被废弃的 API 的警告:

java
@SuppressWarnings("deprecation")

如上所述,该注解仅抑制普通废弃警告,而不是删除警告。

如果需要在特定位置同时抑制普通废弃警告和删除警告,则可以使用以下结构:

java
@SuppressWarnings({"deprecation", "removal"})

下面是从上一节修改后的警告表格副本,显示了如何抑制不同情况下的警告。

java
    use site     |      API declaration site
    context      | not dep.   ord. dep.   term. dep.
                 +----------------------------------
    not dep.     |    -        @SW(d)       @SW(r)
                 |
    ord. dep.    |    -           -         @SW(r)
                 |
    term. dep.   |    -           -         @SW(r)

        @SW(d) = @SuppressWarnings("deprecation")
        @SW(r) = @SuppressWarnings("removal")

如果使用终止废弃的 API 的使用位置上使用 @SuppressWarnings("removal") 抑制了删除警告,并且该 API 更改为普通废弃,则普通废弃警告的出现可能有些奇怪。但是,我们预计 API 从终止废弃返回到普通废弃的演变路径非常罕见。

JLS 第 9.6.4.6 节需要相应地进行修改,该修改由 JDK-8145716 覆盖。

静态分析

提供了一个名为 jdeprscan 的静态分析工具,用于扫描 JAR 文件(或其他包含类文件的聚合)以检查使用已废弃的 API 元素的情况。默认情况下,被废弃的 API 将是 Java SE 本身中的废弃项。未来的扩展将提供扫描在 Java SE 之外的类库中已声明的废弃项的能力。

未来工作的想法

可以提供一种名为 jdeprdetect 的动态分析工具,用于跟踪已废弃 API 的动态使用情况。它可以通过使用 Java 代理来实现,对已废弃的 API 元素进行检测,并在检测到使用这些元素时发出警告消息。

动态分析有助于捕获静态分析所忽略的情况。这些情况包括对已废弃 API 的反射访问,或通过 ServiceLoader 加载已废弃提供程序的使用。此外,动态分析可以显示可能由静态分析标记的依赖项的 缺失。例如,代码可能引用已废弃的 API,而此引用将导致 jdeprscan 发出警告。但是,如果代码引用已废弃的 API 是死代码,则 jdeprdetect 将不会发出任何警告。这些信息应帮助开发人员确定其代码迁移工作的优先级。

某些功能完全驻留在库实现中,并且不会在任何公共 API 中显示。其中一个例子是“传统归并排序”算法。有关更多信息,请参见 Java SE 7 和 JDK 7 兼容性。已废弃功能的库实现应能够检查各种系统属性,以确定是否发出运行时日志记录消息,如果是,则该日志消息应采取什么形式。这些属性可能包括:

  • java.deprecation.enableLogging布尔值,默认为 false

    如果为 true,由 Boolean.parseBoolean 方法确定,则库代码将记录废弃的消息。将使用通过调用 System.getLogger() 获得的记录器记录消息,并将使用 System.Logger.Level.WARNING 级别记录消息。

  • java.deprecation.enableStackTrace布尔值,默认为 false

    如果为 true,并且启用了废弃的记录日志,则日志消息将包括堆栈跟踪。

实施和增强其他工具超出了本 JEP 的范围。以下是对这样的工具增强的一些想法,作为未来工作的建议。

可以增强 javadoc 工具来处理 @Deprecated 注释的详细代码。它还可以提供更突出的显示 Detail 值。@deprecatedJavadoc 标记的处理应基本保持不变,尽管可能会略微修改,以包括有关 forRemovalsince 值的信息。

可以修改标准 doclet 以以不同的方式处理已废弃的 API。例如,类的已废弃成员可能会放在单独的选项卡中,放在现有实例、抽象和具体方法的选项卡旁边。已废弃的类可以移到包框架的单独部分中。当前,它包含接口、类、枚举、异常、错误和注解类型的部分。可以添加用于已废弃成员的新部分。

可以进一步增强已废弃 API 的列表。 (可以通过每个页面顶部的链接到达此页面,在包含链接概述、包、类、用途、树形结构、已废弃、索引和帮助的栏中。)该页面目前按种类组织:接口、类、异常、注解类型、字段、方法、构造函数和注解类型元素。包含值 forRemoval=true 的 API 元素应突出显示,因为它们即将被删除,可能产生很大的影响。

增强的 @Deprecated 注释将影响其他工具,例如 IDE。例如,应默认情况下从 IDE 的自动完成菜单和对话框中删除废弃的 API。或者,可以提供自动重构规则,将对已废弃 API 的调用替换为对其替代品的调用。

备选方案

提出了一组备选方案,包括让 JVM 停止、禁用已废弃的功能或使对已废弃 API 的使用在编译时引发错误,除非提供了特定于版本的选项。所有这些提案只能在开发人员首次使用已废弃功能时成功通知开发人员,因为正常的程序(或构建)流在此时中断。因此,后续对已废弃功能的使用可能会被忽略。在遇到此类错误时,大多数开发人员可能会简单地提供特定于版本的选项以启用已废弃的功能。因此,总体而言,这种方法无法成功地提供有关应用程序使用的 所有 已废弃功能的开发人员信息。

有人建议将 @deprecated Javadoc 标记退役,采用 @Deprecated 注解。@deprecated Javadoc 标记和 @Deprecated 注解应始终同时存在或同时不存在。然而,在非常抽象的概念意义上,它们是多余的。@deprecated Javadoc 标记提供了描述性文本、理由以及有关替代 API 的信息和链接。这些信息非常适合包含在 javadoc 文档中,因为 javadoc 已经具备了相应的功能(例如链接标记)。将这样的文本信息移入注解值中将需要 javadoc 从注解中提取信息,而不是从文档注释中。这对开发人员来说更难维护,因为注解不支持标记语言。最后,注解元素在运行时占用空间,而在运行时存在文档文本是不必要的。

已提出将字符串值作为详细代码。这似乎提供了更大的灵活性,但也引入了弱类型和命名空间冲突的问题,可能导致未检测到的错误。

@Deprecated 注解中的 "replacement" 元素在本提案的早期版本中存在。其目的是表示替代被废弃 API 的特定 API。实际上,任何废弃 API 都没有一个可以直接替代的 API;总是存在权衡和设计考虑,或者在几个可能的替代品之间需要做出选择。所有这些主题都需要讨论,因此更适合于文本文档。最后,注解元素没有指向另一个 API 的语法,而 Javadoc 已经通过其 @see@link 标签支持此类引用。

本提案的早期版本包括各种“reason”代码,包括 UNSPECIFIED、DANGEROUS、OBSOLETE、SUPERSEDED、UNIMPLEMENTED 和 EXPERIMENTAL。它们试图对 API 废弃的原因、使用它的风险以及是否有替代 API 进行编码。实际上,所有这些信息都过于主观,无法作为注解中的值进行编码。相反,这些信息应在 Javadoc 文档注释中进行描述。唯一重要的细节是是否打算删除 API。这由 forRemoval 注解元素表示。

测试

将为新工具构建一组相当简单的测试。提供了一组不同种类的 API 元素,对每个元素进行了废弃。还将构建另一组情况,其中包含上述案例中每个已废弃 API 的使用。应运行静态分析检查器 jdeprscan,以确保它对所有此类使用发出警告。