Effective Java 第三版——56. 为所有已公开的API元素编写文档注释

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java, Third Edition

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

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

虽然文档注释约定不是Java语言的正式一部分,但它们构成了每个Java程序员都应该知道的事实上的API。“如何编写文档注释(How to Write Doc Comments web page)”的网页[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标签后面的短语或子句不以句号结束。以下的文档注释说明了所有这些约定:

/**
 * Returns the element at the specified position in this list.
 *
 * <p>This method is <i>not</i> guaranteed to run in constant
 * time. In some implementations it may run in time proportional
 * to the element position.
 *
 * @param  index index of element to return; must be
 *         non-negative and less than the size of this list
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException if the index is out of range
 *         ({@code index < 0 || index >= this.size()})
 */
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调用方法,那么允许子类依赖于实现行为。下面是实际应用中的实例:

  /**
 * Returns true if this collection is empty.
 *
 * @implSpec
 * This implementation returns {@code this.size() == 0}.
 *
 * @return true if this collection is empty
 */
public boolean isEmpty() { ... }

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

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

* 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}标签来包围不愉快的句点和任何相关的文本,这样源代码中的句点后面就不会有空格了:

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

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

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

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

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

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

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

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

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

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

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

/**
 * An instrument section of a symphony orchestra.
 */
public enum OrchestraSection {
    /** Woodwinds, such as flute, clarinet, and oboe. */
    WOODWIND,

    /** Brass instruments, such as french horn and trumpet. */
    BRASS,

    /** Percussion instruments, such as timpani and cymbals. */
    PERCUSSION,

    /** Stringed instruments, such as violin and cello. */
    STRING;
}

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

/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to pass.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
     /**
      * The exception that the annotated test method must throw
      * in order to pass. (The test is permitted to throw any
      * subtype of the type described by this class object.)
      */
    Class<? extends Throwable> value();
}

包级别文档注释应放在名为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的元字符。

posted @ 2019-03-12 17:53  林本托  阅读(673)  评论(0编辑  收藏  举报