56. 为所有已公开的 API 元素编写文档注释

  如果 API 要可用,就必须对其进行文档化。传统上,API 文档是手工生成的,保持文档与代码的同步是一件苦差事。Java 编程环境使用 Javadoc 实用程序简化了这一任务。Javadoc 使用特殊格式的文档注释 (通常称为 doc 注释),从源代码自动生成 API 文档。

  虽然文档注释约定不是 Java 语言的正式一部分,但它们构成了每个 Java 程序员都应该知道的事实上的 API。「如何编写文档注释(How to Write Doc Comments)」的网页[Javadoc-guide] 中介绍了这些约定。 虽然自 Java 4 发布以来该页面尚未更新,但它仍然是一个非常宝贵的资源。 Java 9 中添加了一个重要的文档标签,{@ index}; Java 8 中有一个,{@implSpec};Java 5 中有两个,{@literal}{@code}。 上述网页中缺少这些标签的介绍,但在此条目中进行讨论。

  要正确地记录 API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释。如果一个类是可序列化的,还应该记录它的序列化形式 (详见第 87 条)。在没有文档注释的情况下,Javadoc 可以做的最好的事情是将声明重现为受影响的 API 元素的唯一文档。使用缺少文档注释的 API 是令人沮丧和容易出错的。公共类不应该使用默认构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为大多数未导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出 API 元素那样完整。

  方法的文档注释应该简洁地描述方法与其客户端之间的契约。除了为继承而设计的类中的方法 (详见第 19 条)之外,契约应该说明方法做什么,而不是它如何工作的。文档注释应该列举方法的所有前置条件 (这些条件必须为真,以便客户端调用它们),以及后置条件(这些条件是在调用成功完成后才为真)。通常,对于未检查的异常,前置条件由 @throw 标签隐式地描述;每个未检查异常对应于一个先决条件违反( precondition violation)。此外,可以在受影响的参数的 @param 标签中指定前置条件。

  除了前置条件和后置条件之外,方法还应在文档中记录它的副作用(side effort)。 副作用是系统状态的可观察到的变化,这对于实现后置条件而言显然不是必需的。 例如,如果方法启动后台线程,则文档应记录它。

  完整地描述方法的契约,文档注释应该为每个参数都有一个 @param 标签,一个 @return 标签 (除非方法有 void 返回类型),以及一个 @throw 标签(无论是检查异常还是非检查异常)(详见第 74 条)。如果 @return 标签中的文本与方法的描述相同,则可以忽略它,这取决于你所遵循的编码标准。

  按照惯例,@param@retur 标签后面的文本应该是一个名词短语,描述参数或返回值所表示的值。 很少使用算术表达式代替名词短语; 请参阅 BigInteger 的示例。@throw 标签后面的文本应该包含单词「if」,后面跟着一个描述抛出异常的条件的子句。按照惯例,@param@return@throw 标签后面的短语或子句不以句号结束。以下的文档注释说明了所有这些约定:

  1. /**
  2. * Returns the element at the specified position in this list.
  3. *
  4. * <p>This method is <i>not</i> guaranteed to run in constant
  5. * time. In some implementations it may run in time proportional
  6. * to the element position.
  7. *
  8. * @param index index of element to return; must be
  9. * non-negative and less than the size of this list
  10. * @return the element at the specified position in this list
  11. * @throws IndexOutOfBoundsException if the index is out of range
  12. * ({@code index < 0 || index >= this.size()})
  13. */
  14. E get(int index);

  请注意在此文档注释(<p><i>)中使用 HTML 标记。 Javadoc 实用工具将文档注释转换为 HTML,文档注释中的任意 HTML 元素最终都会生成 HTML 文档。 有时候,程序员甚至会在他们的文档注释中嵌入 HTML 表格,尽管这种情况很少见。

  还要注意在@throw子句中的代码片段周围使用 Javadoc 的 {@code}标签。这个标签有两个目的:它使代码片段以代码字体形式呈现,并且它抑制了代码片段中 HTML 标记和嵌套 Javadoc 标记的处理。后一个属性允许我们在代码片段中使用小于号(<),即使它是一个 HTML 元字符。要在文档注释中包含多行代码示例,请使用包装在 HTML <pre>标记中的 Javadoc{@code}标签。换句话说,在代码示例前面加上字符<pre>{@code,然后在代码后面加上}</pre>。这保留了代码中的换行符,并消除了转义 HTML 元字符的需要,但不需要转义 at 符号(@),如果代码示例使用注释,则必须转义 at 符号(@)。

  最后,请注意文档注释中使用的单词「this list」。按照惯例,「this」指的是在实例方法的文档注释中,指向方法调用所在的对象。

  正如条目 15 中提到的,当你为继承设计一个类时,必须记录它的自用模式(self-use patterns),以便程序员知道重写它的方法的语义。这些自用模式应该使用在 Java 8 中添加的@implSpec 标签来文档记录。回想一下,普通的问问昂注释描述了方法与其客户端之间的契约;相反,@implSpec 注释描述了方法与其子类之间的契约,如果它继承了方法或通过 super 调用方法,那么允许子类依赖于实现行为。下面是实际应用中的实例:

  1. /**
  2. * Returns true if this collection is empty.
  3. *
  4. * @implSpec
  5. * This implementation returns {@code this.size() == 0}.
  6. *
  7. * @return true if this collection is empty
  8. */
  9. public boolean isEmpty() { ... }

  从 Java 9 开始,Javadoc 实用工具仍然忽略 @implSpec 标签,除非通过命令行开关:-tag "implSpec:a:Implementation Requirements:"。希望在后续的版本中可以修正这个错误。

  不要忘记,你必须采取特殊操作来生成包含 HTML 元字符的文档,例如小于号(<),大于号(>)和 and 符号(&)。 将这些字符放入文档的最佳方法是使用{@literal}标签将它们包围起来,该标签禁止处理 HTML 标记和嵌套的 Javadoc 标记。 它就像{@code}标签一样,除了不会以代码字体呈现文本以外。 例如,这个 Javadoc 片段:

  1. * A geometric series converges if {@literal |r| < 1}.

  它会生成文档:「A geometric series converges if |r| < 1.」。{@literal}标签可能只放在小于号的位置,而不是整个不等式,并且生成的文档是一样的,但是文档注释在源代码中的可读性较差。 这说明了文档注释在源代码和生成的文档中都应该是可读的通用原则。 如果无法实现这两者,则生成的文档的可读性要胜过在源代码中的可读性。

  每个文档注释的第一个「句子」(如下定义)成为注释所在元素的概要描述。 例如,第 255 页上的文档注释中的概要描述为:「返回此列表中指定位置的元素」。概要描述必须独立描述其概述元素的功能。 为避免混淆,类或接口中的两个成员或构造方法不应具有相同的概要描述。 要特别注意重载方法,为此通常使用相同的第一句话是自然的(但在文档注释中是不可接受的)。

  请小心,如果预期的概要描述包含句点,因为句点可能会提前终止描述。例如,以「A college degree, such as B.S., M.S. or Ph.D.」 会导致概要描述为「A college degree, such as B.S., M.S」。问题在于概要描述在第一个句点结束,然后是空格、制表符或行结束符(或第一个块标签处)[Javadoc-ref]。这里是缩写「M.S.」 中的第二个句号后面跟着一个空格。最好的解决方案是用{@literal}标签来包围不愉快的句点和任何相关的文本,这样源代码中的句点后面就不会有空格了:

  1. /**
  2. * A college degree, such as B.S., {@literal M.S.} or Ph.D.
  3. */
  4. public class Degree { ... }

  说概要描述是文档注释中的第一句子,其实有点误导人。按照惯例,它很少应该是一个完整的句子。对于方法和构造方法,概要描述应该是一个动词短语 (包括任何对象),描述了该方法执行的操作。例如:

  • ArrayList(int initialCapacity) —— 构造具有指定初始容量的空列表。
  • Collection.size() —— 返回此集合中的元素个数。

  如这些例子所示,使用第三人称陈述句时态 (“returns the number”)而不是第二人称祈使句(“return the number”)。

  对于类,接口和属性,概要描述应该是描述由类或接口的实例或属性本身表示的事物的名词短语。 例如:

  • Instant —— 时间线上的瞬时点。
  • Math.PI—— 更加接近 pi 的 double 类型数值,即圆的周长与其直径之比。

  在 Java 9 中,客户端索引被添加到 Javadoc 生成的 HTML 中。这个索引以页面右上角的搜索框的形式出现,它简化了导航大型 API 文档集的任务。当你在框中键入时,得到一个匹配页面的下拉菜单。API 元素 (如类、方法和属性) 是自动索引的。有时,可能希望索引对你的 API 很重要的其他术语。为此添加了{@index}标签。对文档注释中出现的术语进行索引,就像将其包装在这个标签中一样简单,如下面的片段所示:

  1. * This method complies with the {@index IEEE 754} standard.

  泛型,枚举和注释需要特别注意文档注释。 记录泛型类型或方法时,请务必记录所有类型参数

  1. /**
  2. * An object that maps keys to values. A map cannot contain
  3. * duplicate keys; each key can map to at most one value.
  4. *
  5. * (Remainder omitted)
  6. *
  7. * @param <K> the type of keys maintained by this map
  8. * @param <V> the type of mapped values
  9. */
  10. public interface Map<K, V> { ... }

  在记录枚举类型时,一定要记录常量,以及类型和任何公共方法。注意,如果文档很短,可以把整个文档注释放在一行:

  1. /**
  2. * An instrument section of a symphony orchestra.
  3. */
  4. public enum OrchestraSection {
  5. /** Woodwinds, such as flute, clarinet, and oboe. */
  6. WOODWIND,
  7. /** Brass instruments, such as french horn and trumpet. */
  8. BRASS,
  9. /** Percussion instruments, such as timpani and cymbals. */
  10. PERCUSSION,
  11. /** Stringed instruments, such as violin and cello. */
  12. STRING;
  13. }

  在为注解类型记录文档时,一定要记录任何成员,以及类型本身。用名词短语表示的文档成员,就好像它们是属性一样。对于类型的概要描述,请使用动词短语,它表示当程序元素具有此类型注解的所表示的含义:

  1. /**
  2. * Indicates that the annotated method is a test method that
  3. * must throw the designated exception to pass.
  4. */
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Target(ElementType.METHOD)
  7. public @interface ExceptionTest {
  8. /**
  9. * The exception that the annotated test method must throw
  10. * in order to pass. (The test is permitted to throw any
  11. * subtype of the type described by this class object.)
  12. */
  13. Class<? extends Throwable> value();
  14. }

  包级别文档注释应放在名为 package-info.java 的文件中。 除了这些注释之外,package-info.java 还必须包含一个包声明,并且可以在此声明中包含注解。 同样,如果使用模块化系统(详见第 15 条),则应将模块级别注释放在 module-info.java 文件中。

  在文档中经常忽略的 API 的两个方面,分别是线程安全性和可序列化性。无论类或静态方法是否线程安全,都应该在文档中描述其线程安全级别,如条目 82 中所述。如果一个类是可序列化的,应该记录它的序列化形式,如条目 87 中所述。

  Javadoc 具有「继承(inherit)」方法注释的能力。 如果 API 元素没有文档注释,Javadoc 将搜索最具体的适用文档注释,接口文档优先于超类文档。 搜索算法的详细信息可以在 The Javadoc Reference Guide [Javadoc-ref] 中找到。 还可以使用{@inheritDoc}标签从超类继承部分文档注释。 这意味着,除其他外,类可以重用它们实现的接口的文档注释,而不是复制这些注释。 该工具有可能减轻维护多组几乎相同的文档注释的负担,但使用起来很棘手并且有一些限制。 详细信息超出了本书的范围。

  关于文档注释,应该添加一个警告说明。虽然有必要为所有导出的 API 元素提供文档注释,但这并不总是足够的。对于由多个相互关联的类组成的复杂 API,通常需要用描述 API 总体架构的外部文档来补充文档注释。如果存在这样的文档,相关的类或包文档注释应该包含到外部文档的链接。

  Javadoc 会自动检查是否符合此条目中的许多建议。在 Java 7 中,需要命令行开关-Xdoclint来获得这种行为。在 Java 8 和 Java 9 中,默认情况下启用了此检查。诸如 checkstyle 之类的 IDE 插件会进一步检查是否符合这些建议[Burn01]。还可以通过 HTML 有效性检查器运行 Javadoc 生成的 HTML 文件来降低文档注释中出现错误的可能性。可以检测 HTML 标记的许多错误用法。有几个这样的检查器可供下载,可以使用 W3C markup validation service 在线验证 HTML 格式。在验证生成的 HTML 时,请记住,从 Java 9 开始,Javadoc 就能够生成 HTML5 和 HTML 4.01,尽管默认情况下仍然生成 HTML 4.01。如果希望 Javadoc 生成 HTML5,请使用-html5命令行开关。

  本条目中描述的约定涵盖了基本内容。尽管撰写本文时已经有 15 年的历史,但编写文档注释的最终指南仍然是《How to Write Doc Comments》[Javadoc-guide]。

  如果你遵循本项目中的指导原则,生成的文档应该提供对 API 的清晰描述。然而,唯一确定的方法,是阅读 Javadoc 实用工具生成的 web 页面。对于其他人将使用的每个 API,都值得这样做。正如测试程序几乎不可避免地会导致对代码的一些更改一样,阅读文档通常也会导致对文档注释的一些少许的修改。

  总之,文档注释是记录 API 的最佳、最有效的方法。对于所有导出的 API 元素,它们的使用应被视为必需的。 采用符合标准惯例的一致风格 。请记住,在文档注释中允许任意 HTML,但必须转义 HTML 的元字符。