跳到主要内容

JEP 277:增强的弃用功能

QWen Max 中英对照

概述​

改进 @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 中移除任何内容变得异常困难。

弃用的另一个问题是警告仅在编译时发出。随着 Java SE 各个版本中 API 被弃用,现有的二进制文件仍然继续依赖并使用这些被弃用的 API,而没有任何警告。如果某个被弃用的 API 在 JDK 发行版中被移除,即使它在之前的一个或多个版本中已被标记为弃用,这对旧应用程序二进制文件的用户来说将是一个不愉快的意外。该应用程序会因链接错误而突然失败,并且从未发出过任何警告。更糟糕的是,开发者没有任何方法可以检查现有的二进制文件是否对已弃用的 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() 的默认值。

@Deprecated 注解在 API 上的存在,是该 API 的作者或维护者向使用者传达的一种信息。通常来说,弃用(deprecation)是一种建议,它提示用户应将使用从被弃用的 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 元素上应该同时存在或同时不存在。仅存在其中一个而缺少另一个会被视为错误。如果某个 API 上存在 @deprecated 标签但缺少 @Deprecated 注解,javac 的 lint 标志 -Xlint:dep-ann 将发出警告。目前,如果情况相反(即存在 @Deprecated 注解但缺少 @deprecated 标签),则不会发出警告;详情请参见 JDK-8141234。

@Deprecated 注解应该对已弃用 API 的行为没有直接影响,而且性能影响也应该可以忽略不计。

在 Java SE 中的用法​

@Deprecated 注解类型出现在 Java SE 中,因此它可以应用于使用 Java SE 平台的任何类库中的 API。这些类库如何使用 @Deprecated 注解类型的精确规则和策略,是由这些库的维护者来决定的事项。建议类库维护者制定并记录此类策略。

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

几个 Java SE API 将添加、更新或删除 @Deprecated 注解。下面列出了在 Java SE 9 中实施的变更。除非另有规定,这里列出的弃用不是为了删除。请注意,这不是 Java SE 9 中弃用的完整列表。

  • 为包装原始类型(Boolean、Integer 等)的构造函数添加 @Deprecated 注解 (JDK-8145468)

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

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

  • 为 java.util.Observable 和 Observer 添加 @Deprecated 注解 (JDK-8154801)

  • 为各种已被取代的安全 API 添加 @Deprecated(forRemoval=true) 注解,包括 java.security.acl (JDK-8157847)、javax.security.cert 和 com.sun.net.ssl (JDK-8157712)、java.security.Certificate (JDK-8157707) 以及 javax.security.auth.Policy (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() 以及各种废弃的 Runtime 和 SecurityManager 方法,为其添加 @Deprecated(forRemoval=true) 注解 (JDK-8145468)

鉴于 Java SE 中废弃(deprecation)的历史,以及对跨版本长期 API 兼容性的重视,移除一个 API 是一件非常严重的事情。因此,仅当在下一版本的 Java SE 平台中有一个明确且确定的计划来移除该 API 时,才应使用 forRemoval=true 的元素进行废弃声明。

除非某个 API 元素在之前版本的 Java SE 中已经使用 @Deprecated(forRemoval=true) 注解标注,否则不应将其从 Java SE 规范中移除。允许在弃用时直接引入 forRemoval=true。在移除 API 之前,不必先使用 forRemoval=false 进行弃用,然后再升级为 forRemoval=true。

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

在 Java SE 9 之前已被弃用的 API 元素,其 since 值将仅在时间允许的情况下填写。(对所有 API 执行此操作的价值有限,主要是一项历史研究的工作。)在这种情况下,用于 since 值的字符串应符合这些发布版本中 @since Javadoc 标签所使用的 JDK 版本约定,通常为 1.0 至 1.8,但有时会带有“微型”发布编号,例如 1.0.2。在 Java SE API 中寻找此值的注解处理工具如果发现空字符串,应假定该弃用发生在 Java SE 8 或更早版本。

弃用 API 将增加项目在基于较新版本的 Java SE 构建时遇到的强制警告数量。包括 JDK 本身在内的一些项目,在编译时使用了启用详细警告并将警告转为错误的编译器选项。对于此类项目,向 Java SE 中添加已弃用的 API 可能会引入大量警告,显著增加迁移到新版本 Java SE 的工作量。现有的用于管理警告的机制,例如 @SuppressWarnings 注解和编译器命令行选项,不足以应对这一问题。这实际上对在特定 Java SE 版本中可以弃用哪些 API 设置了限制,并使得弃用过时但流行的 API 几乎变得不可能。因此,有必要在未来改进管理弃用警告的可用机制。

forRemoval 对警告策略的影响​

Java 语言规范,第 9.6.4.6 节 规定了特定的警告行为,这些行为取决于被依赖的 API 的弃用状态(“声明位置”),以及使用该 API 的代码的弃用状态(“使用位置”)。forRemoval 元素的引入增加了另一组必须定义的情况。为简洁起见,我们将 forRemoval=false 的弃用称为“普通弃用”,将 forRemoval=true 的弃用称为“最终弃用”。

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

使用位置状态声明位置状态是否发出警告
非弃用非弃用否
非弃用弃用是
弃用非弃用否
弃用弃用否
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 的使用点发出的警告,正式名称可能是“终端弃用警告”,但这个说法相当冗长。因此,我们将把此类警告称为“移除警告”。

下面显示的是建议的案例表:

下面显示的是建议的案例表:
markdown
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)类似。有人可能会认为,由于使用处和声明处都被终止弃用,二者都将“消失”,因此在此处发出警告毫无意义。但存在这样一种可能性:声明处位于一个比使用处发展更快的库中,因此使用处的生命周期可能比声明处更长。因此,有必要对声明处即将被移除发出警告。

涵盖右下角四个元素的通用规则如下。如果使用站点已被弃用,无论是普通弃用还是终端弃用,都不会发出普通的弃用警告,但仍然会发出移除警告。

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

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

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

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 而产生的警告——可以通过注解来抑制。

@SuppressWarnings("removal")

此注解仅抑制移除警告,而不抑制普通的弃用警告。我们曾考虑过让此注解成为一种强效的抑制形式,能够同时覆盖普通的弃用警告和移除警告。然而,这可能会导致错误。程序员可能使用 @SuppressWarnings("removal") 来抑制来自普通弃用的警告。如果一个普通的弃用被更改为最终弃用,这将阻止警告的出现,从而在最终弃用的 API 被移除时引发意外的破坏。

像以前一样,可以使用注解来抑制通常不推荐使用的 API 所产生的警告。

@SuppressWarnings("deprecation")

如上所述,此注解仅抑制普通的弃用警告;它不会抑制移除警告。

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

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

以下是前一节中警告表的副本,经过修改以展示如何抑制来自不同案例的警告。

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 的代码是无用代码(dead code),则 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 值。对于 @deprecated Javadoc 标签的处理应该基本保持不变,尽管可能会进行一些修改,以包含有关 forRemoval 和 since 值的信息。

标准的文档工具可以进行修改,以对已弃用的 API 进行不同的处理。例如,类的已弃用成员可能会被放入一个单独的标签页中,与现有的实例方法、抽象方法和具体方法标签页并列。已弃用的类可以被移到包框架中的一个单独部分。目前,包框架包含接口、类、枚举、异常、错误和注解类型的部分。可以添加新的部分来展示已弃用的成员。

废弃 API 的列表也可以得到改进。(此页面通过每页顶部栏中包含的链接访问,该栏包含 Overview、Package、Class、Use、Tree、Deprecated、Index、Help 等链接。)当前此页面按种类组织:接口、类、异常、注解类型、字段、方法、构造函数和注解类型元素。包含值 forRemoval=true 的 API 元素应该被突出显示,因为它们即将被移除,可能会产生重大影响。

增强的 @Deprecated 注解将影响其他工具,例如 IDE。例如,默认情况下,不推荐使用的 API 应该不出现在 IDE 的自动完成菜单和对话框中。或者,可以提供自动重构规则,将对不推荐使用 API 的调用替换为对其替代项的调用。

替代方案​

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

有人建议废弃 @deprecated Javadoc 标签,转而使用 @Deprecated 注解。@deprecated Javadoc 标签和 @Deprecated 注解应当始终同时存在或同时不存在。然而,它们仅在非常抽象、概念化的意义上是冗余的。@deprecated Javadoc 标签提供了描述性文字、理由以及指向替代 API 的信息和链接。这类信息非常适合包含在 javadoc 文档中,因为后者已经具备相应的功能(例如链接标签)。将此类文本信息移至注解值中会要求 javadoc 从注解而非文档注释中提取信息。这会使开发者更难以维护,因为注解不支持标记语言。最后,注解元素会在运行时占用空间,而文档文本在运行时存在于内存中是没有必要的。

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

在本提案的早期版本中,@Deprecated 注解中包含一个名为“replacement”(替代)的元素。其目的是用它来指代一个特定的 API,以替代被弃用的那个 API。然而在实践中,没有任何一个被弃用的 API 能够找到完全无缝替代的 API;总是会存在权衡、设计考量,或者需要从多个可能的替代方案中做出选择。所有这些主题都需要详细讨论,因此更适合通过文本文档来呈现。最后,注解元素中并没有用于引用其他 API 的语法,而 Javadoc 已经通过其 @see 和 @link 标签支持了这种引用功能。

此提案的先前版本包含各种“原因”代码,包括 UNSPECIFIED、DANGEROUS、OBSOLETE、SUPERSEDED、UNIMPLEMENTED 和 EXPERIMENTAL。这些代码试图对 API 被弃用的原因、使用它的风险进行编码,同时还表明是否存在替代的 API。然而在实践中,所有这些信息都过于主观,无法作为注解中的值来编码。相反,这些信息应当在 Javadoc 文档注释中进行描述。剩下的唯一重要细节是是否有意图移除该 API。这一点通过 forRemoval 注解元素来表达。

测试​

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