跳到主要内容

JEP 277:增强弃用

概括

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

目标

  • 提供有关规范中 API 的状态和预期处置的更好信息。

  • 提供一个工具来分析应用程序对已弃用 API 的静态使用情况。

非目标

@deprecated将Javadoc 标记与注释统一起来并不是该项目的目标@Deprecated

动机

弃用是一种传达有关 API 生命周期信息的技术:鼓励应用程序从 API 迁移,阻止应用程序对 API 形成新的依赖关系,并告知开发人员继续依赖 API 的风险。

Java 提供了两种机制来表达弃用:@deprecatedJDK 1.1 中引入的 Javadoc 标记和@DeprecatedJava SE 5 中引入的注释。注释的 API 规范@Deprecated(反映在 Java 语言规范中)为:

带注释的程序元素@Deprecated是程序员不鼓励使用的元素,通常是因为它很危险,或者因为存在更好的替代方案。当在未弃用的代码中使用或覆盖已弃用的程序元素时,编译器会发出警告。

然而,该@Deprecated注释最终被用于多种不同的目的。实际上,很少有已弃用的 API 被删除,这导致一些人认为永远不会删除任何内容。另一方面,其他人认为所有被弃用的东西最终都可能被删除,这也从来不是意图。 (尽管规范中没有明确说明,但各种文档都提到过时的 API 将在某个时候被删除。)这导致向开发人员传递了一条不明确的消息,内容涉及 的含义@Deprecated,以及开发人员应该做什么(如果有的话)当他们遇到使用已弃用的 API 时。每个人都对弃用的真正含义感到困惑,没有人认真对待它。这反过来又使得从 Java SE API 中删除任何内容变得困难。

弃用的另一个问题是仅在编译时发出警告。随着 API 在 Java SE 的后续版本中被弃用,现有的二进制文件将继续依赖并使用已弃用的 API,且不会发出任何警告。如果要在 JDK 版本中删除已弃用的 API,即使在一个或多个已弃用该 API 的版本之后,这也会给旧应用程序二进制文件的用户带来不愉快的意外。应用程序会突然因链接错误而失败,并且不会发出任何警告。更糟糕的是,开发人员无法检查现有的二进制文件是否依赖于已弃用的 API。这会导致在新 JDK 版本上运行旧二进制文件的能力与通过淘汰旧 API 来发展规范的需求之间存在巨大的紧张关系。

综上所述,Java SE API 中弃用机制的应用并不一致,导致人们对弃用原则上的含义以及实践中如何正确使用弃用感到困惑。

描述

规格

增强注释的主要目的@Deprecated是向工具提供有关 API 弃用状态的更细粒度的信息。这些工具又使用注释向 API 用户报告信息。该@Deprecated注释具有运行时保留,因此会消耗堆内存。因此,此处的信息应该最少且明确。

以下元素将添加到java.lang.Deprecated注释类型中:

  • 返回forRemoval()一个boolean.如果true,则表示此 API 元素已指定在未来版本中删除。如果false,则该 API 元素已被弃用,但目前无意在未来版本中删除它。该元素的默认值为false

  • since()名为Returning 的方法String。此字符串应包含此 API 被弃用的发行版或版本号。它具有自由格式的语法,但版本编号应遵循与@since包含已弃用 API 的项目的 Javadoc 标记相同的方案。请注意,此值与 Javadoc标记_并不_@since多余,因为它记录了引入 API 的版本,而注释since()中的方法@Deprecated记录了弃用 API 的版本。该元素的默认值为空字符串。

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

API 上注释的存在@Deprecated是 API 的作者或维护者与 API 用户之间的通信。最常见的是,弃用是建议用户将其使用从已弃用的 API 中迁移出来,避免从新代码或维护旧代码时添加对此 API 的依赖项,或者维护依赖于此 API 的代码存在一定的风险API。推荐这种迁移的理由有很多。原因可能包括以下几点:

  • API 有缺陷且无法修复,

  • API 的使用可能会导致错误,

  • 该 API 已被另一个 API 取代,

  • API 已过时,

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

  • 或以上的任意组合。

弃用 API 的确切原因通常过于微妙,无法用注释中的标志或元素值来表达。强烈建议在 API 的文档注释中描述弃用 API 的原因。此外,还建议讨论潜在的替代 API 并从文档中链接出来。

然而,提供了一个特定的标志值。布尔元素forRemoval()iftrue表示 API 元素将在项目的未来版本中删除的意图。因此,API 的用户会收到提前警告,如果他们不迁移 API,他们的代码在升级到新版本时可能会被破坏。如果forRemoval()false,则表示建议迁移出已弃用的 API,但没有任何删除该 API 的具体意图。

API 元素上应同时存在或不存在注释和 javadoc标记@Deprecated@deprecated一个人的存在而没有另一个人的存在被认为是一种错误。如果标签出现在缺少注释的 API 上,javaclint 标志将发出警告。如果情况相反,目前不会发出警告;请参阅JDK-8141234-Xlint:dep-ann``@deprecated``@Deprecated

注释@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 中弃用的完整列表。

  • 添加@Deprecated到盒装基元的构造函数(BooleanInteger等)( JDK-8145468 )

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

  • 添加@Deprecated到各种java.applet相关类(JEP 289

  • 添加@Deprecatedjava.util.Observableand Observer( JDK-8154801 )

  • 添加@Deprecated(forRemoval=true)到各种被取代的安全 API,包括java.security.acl( JDK-8157847 )、javax.security.cert( com.sun.net.sslJDK -8157712 )、java.security.Certificate( JDK-8157707 ) 和javax.security.auth.Policy( JDK-8157848 )

  • 添加@Deprecated(forRemoval=true)java.lang.CompilerJDK-4285505

  • 添加@Deprecated到几个Java EE模块和java.corba模块(JDK-8169069JDK-8181195JDK-8181702JDK-8174728

  • 修改已经弃用的方法、、、、Thread.destroy()以及各种不使用的方法(JDK - 8145468 )Thread.stop(Throwable)``Thread.countStackFrames()``System.runFinalizersOnExit()``Runtime``SecurityManager``@Deprecated(forRemoval=true)

鉴于 Java SE 弃用的历史,以及对跨版本 API 长期兼容性的重视,删除 API 是一个值得严重关注的问题。因此,forRemoval=true只有当有明确的计划在 Java SE 平台的下一个版本中删除该 API 时,才应弃用该元素。

API 元素不应从 Java SE 规范中删除,除非它是@Deprecated(forRemoval=true)在 Java SE 的早期版本中通过注释交付的。使用 引入弃用是可以接受的forRemoval=true。在删除 API 之前,无需先弃用forRemoval=false, 然后升级到, 。forRemoval=true

对于 Java SE 9 及更高版本中弃用的 API 元素,该since元素应包含 Java SE 版本字符串,表示 API 元素已弃用的版本。版本字符串应符合JEP 223中指定的格式。由于 Java SE 通常仅在主要版本中进行规范更改,因此版本字符串通常仅包含“MAJOR”版本号。因此,对于 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)这是一个奇怪的案例。如果使用和声明站点均已弃用,则不会发出警告。如果两个站点都位于作为一个单元维护和发布的单个类库中,那么这是有意义的。由于它们是一起维护的,因此在这种情况下发出警告没有什么意义。但是,如果使用站点位于与声明站点分开维护的类库中,则它们可能会以不同的速率发展,因此在这种情况下不发出警告可能是一个错误功能。然而,@SuppressWarnings在 Java SE 5 中引入注释之前,这种机制对于减少 JDK 编译产生的警告数量很有用。

(JLS 9.6.4.6 还要求,如果使用地点与声明地点位于同一最外层类别,则无需发出警告。在这种情况下,使用地点和声明地点根据定义保持在一起,因此不发出警告的理由适用出色地。)

在 Java SE 9 中,引入的forRemoval添加了一些与终端弃用有关的新情况。这需要引入一种新的警告。

在使用通常弃用的 API 时发出的警告是“普通弃用警告”,与 Java SE 8 及更早版本中的警告相同。这些通常被简单地称为“弃用警告”,作为以前使用的保留。

在使用最终弃用的 API 时发出的警告可能正式称为“终端弃用警告”,但这相当冗长。相反,我们将此类警告称为“删除警告”。

拟议的案例表如下所示:

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 本身已弃用的 API。未来的扩展将提供扫描已在 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。 Javadoc 标记的处理@deprecated应该基本上没有变化,尽管可能会进行一些修改以包含有关forRemovalsince值的信息。

可以修改标准 doclet 以以不同的方式处理已弃用的 API。例如,类中已弃用的成员可能会与现有选项卡(例如,抽象方法和具体方法)一起放入单独的选项卡中。已弃用的类可以移至包框架中的单独部分。目前,它包含接口、类、枚举、异常、错误和注释类型部分。可以添加针对已弃用成员的新部分。

已弃用的 API 列表也可以得到增强。 (通过每个页面最顶部的链接访问此页面,该栏中包含链接概述、包、类、使用、树、已弃用、索引、帮助。)此页面当前按类型组织:接口、类、异常、注释类型、字段、方法、构造函数和注释类型元素。应突出显示包含该值的 API 元素forRemoval=true,因为即将删除它们可能会产生巨大影响。

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

备择方案

已提出的一组替代方案包括停止 JVM、禁用已弃用的功能或使用已弃用的 API 导致编译时错误,除非提供特定于版本的选项。所有这些建议只有在通知开发人员_首次_使用已弃用的功能时才会成功,因为正常的程序(或构建)流程在此时会被中断。因此,后续使用已弃用的功能可能不会被发现。遇到此类故障时,大多数开发人员只会提供特定于版本的选项来启用已弃用的功能。因此,一般来说,这种方法无法成功地向开发人员提供有关应用程序使用的_所有已弃用功能的信息。_

有人建议@deprecated取消 Javadoc 标签以支持@Deprecated注释。 Javadoc标记@deprecated@Deprecated注释应该始终存在或不存在。然而,它们仅在非常抽象的概念意义上是多余的。 Javadoc标签@deprecated提供描述性文本、基本原理、信息以及替换 API 的链接。该信息非常适合包含在 javadoc 文档中,该文档已经具有相应的功能(例如链接标记)。将此类文本信息移入注释值将需要 javadoc 从注释而不是文档注释中提取信息。由于注释没有标记支持,因此开发人员维护起来会更加困难。最后,注释元素在运行时占用空间,并且文档文本没有必要在运行时存在于内存中。

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

注释中的“替换”元素@Deprecated出现在该提案的早期版本中。它的目的是表示一个特定的 API 来取代已弃用的 API。实际上,任何已弃用的 API 都不会存在直接替代 API;总是需要权衡和设计考虑,或者在几种可能的替代方案中做出选择。所有这些主题都需要讨论,因此更适合文本文档。最后,没有用于从注释元素引用另一个 API 的语法,而 Javadoc 已经通过其@see@link标签支持此类引用。

该提案的先前版本包括各种“原因”代码,包括未指定、危险、过时、已取代、未实现和实验。这些尝试对 API 被弃用的原因、使用它的风险以及是否有替代 API 可用进行编码。实际上,所有这些信息都过于主观,无法编码为注释中的值。相反,应在 Javadoc 文档注释中描述此信息。剩下的唯一重要的细节是是否有意删除该 API。这在注释元素中表达forRemoval

测试

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