分享

带你快速看完9.8分神作《Effective Java》—— 方法篇

 小王曾是少年 2022-08-24 发布于江苏

🍊 Java学习:Java从入门到精通总结

🍊 Spring系列推荐:Spring源码解析

📆 最近更新:2021年12月16日

🍊 个人简介:通信工程本硕💪、阿里新晋猿同学🌕。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤

🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要外在的武功招式(编程语言、工具、中间件等等),也需要修炼心法(设计模式、源码等等)学霸、学神OR开挂

我也始终有一个观点:看视频跟着敲代码永远只是入门,从书籍里学到了多少东西才决定了你的上限。

在这里插入图片描述

我个人在Java领域也已经学习了近5年,在修炼“内功”的方面也通过各种途径接触到了一些编程规约,例如阿里巴巴的泰山版规约,在此基础下读这本书的时候仍是让我受到了很大的冲激,学习到了很多约定背后的细节问题,还有一些让我欣赏此书的点是,书中对于编程规约的解释让我感到十分受用,并愿意将他们应用在我的工作中,也提醒了我要把阅读JDK源码的任务提上日程。

最后想分享一下我个人目前的看法,内功修炼不像学习一个新的工具那么简单,其主旨在于踏实,深入探索底层原理的过程很缓慢并且是艰辛的,但一旦开悟,修为一定会突破瓶颈,达到更高的境界,这远远不是我通过一两篇博客就能学到的东西。

接下来就针对此书列举一下我的收获与思考。

不过还是要吐槽一下的是翻译版属实让人一言难尽,有些地方会有误导的效果,你比如java语言里extends是继承的关键字,书本中全部翻译成了扩展 就完全不是原来的意思了。所以建议有问题的地方对照英文原版进行语义上的理解。

没有时间读原作的同学可以参考我这篇文章。


文章目录

49 检查参数的有效性

当编写方法或构造方法时,都应该考虑其参数应该有哪些限制。应该把这些限制写到文档里,并在方法体的开头显式检查


大多数方法和构造方法对于传递给他们的参数有一些限制。例如,索引值必须是非负数,对象引用必须为非null。我们应该在文档里清楚地指明这些限制,并且在方法的最开始进行检查。

如果没有验证参数的有效性,可能会导致违背失败原子性

  1. 该方法可能在处理过程中失败,该方法可能会出现费解的异常
  2. 该方法可以正常返回,会默默地计算出错误的结果
  3. 该方法可以正常返回,但是使得某个对象处于受损状态,在将来某个时间点会报错

对于publicprotected方法,要用Java文档的@throws注解来说明会抛出哪些异常,通常为:IllegalArgumentExceptionIndexOutOfBoundsExceptionNullPointerException,例如:

/**
 * Returns a BigInteger whose value is (this mod m). This method
 * differs from the remainder method in that it always returns a
 * non-negative BigInteger.
 *
 * @param m the modulus, which must be positive
 * @return this mod m
 * @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
	if (m.signum() <= 0)
		throw new ArithmeticException("Modulus <= 0: " + m);
	
	... // Do the computation
}

在Java 7中添加的 Objects.requireNonNull 方法灵活方便,因此没有理由再手动执行null检查。该方法返回其输入的值,因此可以在使用值的同时执行null检查:

this.strategy = Objects.requireNonNull(strategy, "strategy");

对于不是public的方法,通常应该使用断言来检查参数:

private static void sort(long a[], int offset, int length) {
	assert a != null;
	assert offset >= 0 && offset <= a.length;
	assert length >= 0 && length <= a.length - offset;
	... // Do the computation
}

不同于一般的有效性检查,如果它们没有起到作用,本质上也没有成本开销。


在某些场景下,有效性检查的成本很高,且在计算过程里也已经完成了有效性检查,例如对象列表排序的方法Collections.sort(List)

如果List里的对象不能互相比较,就会抛ClassCastException异常,这正是sort方法该做的事情,所以提前检查列表中的元素是否可以互相比较并没有很大意义。


有些计算会隐式执行必要的有效性检查,如果检查失败则会抛异常,这个异常可能和文档里标明的不同,此时就应该使用异常转换将其转换成正确的异常。


50 必要时进行保护性拷贝

Java是一门安全的语言,它对于缓存区溢出、数组越界、非法指针以及其他内存损坏错误都自动免疫。


但仅管如此,我们也必须保护性地编写程序,因为代码随时可能会遭受攻击

如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但对象可能会在无意的情况下提供这样的帮助。例如,下面的代码表示一个不可变的时间周期:

public final class Period {
	private final Date start;
	private final Date end;
	
	/**
	* @param start the beginning of the period
	* @param end the end of the period; must not precede start
	* @throws IllegalArgumentException if start is after end
	* @throws NullPointerException if start or end is null
	*/
	public Period(Date start, Date end) {
		if (start.compareTo(end) > 0)
			throw new IllegalArgumentException(start + " after " + end);
		
		this.start = start;
		this.end = end;
	}
	
	public Date start() {
		return start;
	}
	
	public Date end() {
		return end;
	}
	... // Remainder omitted
}

上面代码虽然强制令period 实例的开始时间小于结束时间。然而,Date 类是可变的,很容易违反这个约束:

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!

从Java 8 开始,解决此问题的显而易⻅的方法是使用 Instant(或LocalDateTimeZonedDateTime)代替Date,因为他们是不可变的。但Date在老代码里仍有使用的地方,为了保护 Period 实例的内部不受这种攻击,可以使用拷⻉来做 Period 实例的组件:

public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
        throw new IllegalArgumentException(this.start + " after " + this.end);
}

有了新的构造方法后,前面的攻击将不会对Period 实例产生影响。注意:保护性拷⻉是在检查参数的有效性之前进行的,且有效性检查是在拷贝实例上进行的

这样做可以避免从检查参数开始到拷贝参数之间的时间段内,其他的线程改变类的参数

也被称作 Time-Of-Check / Time-Of-Use 或 TOCTOU攻击


看了之前章节的同学可能有疑问了,这里为什么没用clone方法来进行保护性拷贝?

答案是:Date不是final的,所以clone方法不能保证返回类确实是 java.util.Date 的对象,也可能返回一个恶意的子类实例。


但是普通方法就不一样了,它们在进行保护性拷贝是允许使用clone方法,原因是我们知道Period内部的Date对象类型确实是java.util.Date

对于参数类型可能被恶意子类化的参数,不要使用 clone 方法进行防御性拷⻉


其实,改变Period实例仍是有可能的:

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!

修改方法也很简单:

public Date start() {
	return new Date(start.getTime());
}

public Date end() {
	return new Date(end.getTime());
}

上面的分析带来的启发是:应该尽量使用不可变对象作为对象内部的组件,这样就不必担心保护性拷⻉。在 Period 示例中,使用Instant(或LocalDateTimeZonedDateTime)。另一个选项是存储Date.getTime() 返回的long类型来代替Date引用。


最后,如果拷贝成本较大的话,并且我们新人使用它的客户端不会恶意修改组件,则可以在文档中指明客户端不得修改受到影响的组件,以此来代替保护性拷贝。


51 谨慎设计方法

这一条介绍了若干经验:

1. 谨慎给方法起名

  • 方法名应该选易于理解的,并且与同一个包里其他名称的风格一致
  • 选择大众认可的名称

2. 不要过于追求提供便利的方法
方法太多会使类难以学习、使用、文档化、维护。只有当一项操作被经常用到时,才考虑为它提供快捷方式(shorthand)

3. 避免过长的参数列表,相同类型的长参数序列格外有害

参数个数不超过4个

有三种技巧可以缩短过长的参数列表:

  1. 把一个方法分解成多个方法,每个方法只需要这些参数的一个子集。例如:java.util.List接口里没有提供在子列表中查找元素的第一个索引和最后一个索引的方法。相反,它提供了 subList 方法,返回子列表。此方法可以与 indexOflastIndexOf 方法结合使用来达到所需的功能。

  2. 创建辅助类用来保存参数的分组。例如:编写一个表示纸牌游戏的类,发现需要两个参数来表示纸牌的点数和花色,这时就可以创建一个类来表示卡片。

  3. 从对象构建到方法调用全都采用Builder模式


4. 优先使用接口作为入参类型
只要有适当的接口可用来定义参数,就优先使用这个接口,而不是使用实现该接口的类。例如:在编写方法时使用Map接口作为参数


5. 对于boolean型参数,优先使用有两个元素的枚举
例如,有一个 Thermometer 类型的静态工厂方法,这个方法的签名需要以下这个枚举的值:

public enum TemperatureScale { FAHRENHEIT, CELSIUS }

Thermometer.newInstance(TemperatureScale.CELSIUS) 不仅比Thermometer.newInstance(true) 更有意义,而且可以在将来的版本中将新的枚举值添加到 TemperatureScale 中,而无需向 Thermometer 添加新的静态工厂。


52 慎用重载

下面这个程序试图将一个集合进行分类:

public class CollectionClassifier {

    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<BigInteger>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

运行结果是打印了三次Unknown Collection为什么会这样呢?

原因就是classify方法被重载了,要调用哪个重载方法是在编译时做出决定的。for循环里参数的编译时类型一直是Collection<?>,所以唯一适合的重载方法是classify(Collection<?> c)


有一个很有意思的事实:重载(overloaded)方法的选择是静态的,重写(overridden)方法的选择是动态的

重写方法的选择是在运行时进行的,依据是被调用的方法所在的对象的运行时类型。


以下面这个例子具体说明:

class Wine {
    String name() {
        return "wine";
    }
}

class SparklingWine extends Wine {
    @Override
    String name() {
        return "sparkling wine";
    }
}

class Champagne extends SparklingWine {
    @Override
    String name() {
        return "champagne";
    }
}

public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = Arrays.asList(
                new Wine(), new SparklingWine(), new Champagne());

        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}

这段代码打印出wine,sparkling wine和champagne,尽管在每次迭代里,实例的编译类型都是Wine,但总是会执行最具体(most specific)的重写方法,也就是在子类上调用的就执行被子类覆盖的方法。


CollectionClassifier示例中,程序的目的是根据参数的运行时类型自动执行适当的方法重载来辨别参数的类型。但方法重载完全没有提供这样的功能,这段代码最佳修改方案是:用单个方法来替换这三个重载的classify方法,代码逻辑里用instanceof判断:

public static String classify(Collection<?> c) {
	return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}

如果API的普通用户根本不知道哪个重载会被调用,使用这样的API就会报错。所以,应该避免混淆使用重载

安全保守的策略是:一个安全和保守的策略是永远不要编写两个具有相同参数数量的重载

因为我们始终可以给方法起不同的名字,避免使用重载


例如,考虑ObjectOutputStream类。对于每个类型,它的write方法都有一种变体,例如writeBoolean(boolean)writeInt(int)writeLong(long)。这种命名模式的另一个好处是,可以为read方法提供相应的名称,例如readBoolean()readInt()readLong()


一个类的多个构造器总是重载的,可以选择导出静态工厂。


对于每一对重载方法,至少要有一个形参在这两个重载中具有「完全不同的」类型。这时主要的混淆根源就没有了。例如ArrayList有接受int的构造方法和接受Collection的构造方法。


Java有一个自动装箱的概念,他们的出现也引入了一些麻烦:

public class SetList {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }

        for (int i = 0; i < 3; i++) {
            set.remove(i);
            list.remove(i);
        }

        System.out.println(set + " " + list);
    }
}

实际上,程序从Set中删除非负值,从List中删除奇数值,并打印 [-3, -2, -1] 和 [-2, 0, 2]。


  • set.remove(i)选择重载了remove(E)方法,执行结果正确
  • list.remove(i)的调用选择重载remove(int i)方法,它将删除列表中指定位置的元素,所以最终打印 [-2, 0, 2]

有两种手段可以解决这个问题:

  1. 强制转换list.remove的参数为Integer
  2. 调用Integer.valueOf(i),将结果传递list.remove方法
for (int i = 0; i < 3; i++) {
	set.remove(i);
	list.remove((Integer) i); // or remove(Integer.valueOf(i))
}

在Java 8中添加Lambda表达式和方法引用以后,进一步增加了重载混淆的可能性。

new Thread(System.out::println).start();

ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

Thread 构造方法调用和submit方法调用看起来很相似,但是前者编译而后者不编译。参数是相同的(System.out::println)。因为sumbit方法有一个带有Callable <T>参数的重载,而Thread构造方法却没有。在submit这里不知道应该调用哪个方法。


在更新现有类时,可能会违反这一条目中的指导原则。例如,从Java 4开始就有一个contentEquals(StringBuffer)方法。在Java 5中,添加了contentEquals(CharSequence)接口。但只要这两个方法返回相同的结果就可以,例如下面的代码:

public boolean contentEquals(StringBuffer sb) {
	return contentEquals((CharSequence) sb);
}

原因是这两个重载互相调用。


Java类库在很大程度上遵循了这一条中的建议,但是有一些类违反了它。例如,String导出两个重载的静态工厂方法valueOf(char[])valueOf(Object),这应该被看成是一种反常行为。


53 慎用可变参数

可变参数方法接受0个或多个指定类型的参数,首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数值放入数组中,最后将数组传递给方法。


例如,这里有一个可变参数方法,返回入参的总和:

static int sum(int... args) {
int sum = 0;
for (int arg : args)
	sum += arg;
return sum;
}

有时,编写一个需要某种类型的一个或多个参数的方法是合适的,而不是0个或者多个。可以在运行时检查数组⻓
度:

static int min(int... args) {
	if (args.length == 0)
		throw new IllegalArgumentException("Too few arguments");
	int min = args[0];
	for (int i = 1; i < args.length; i++)
		if (args[i] < min)
			min = args[i];
	return min;
}

最严重的是,如果客户端在没有参数的情况下调用此方法,则它在运行时而不是在编译时失败。

有一种更好的方法可以达到预期的效果。声明方法采用两个参数,一个指定类型的普通参数,另一个此类型的可变参数。

static int min(int firstArg, int... remainingArgs) {
	int min = firstArg;
	for (int arg : remainingArgs)
		if (arg < min)
			min = arg;
	return min;
}

在性能关键的情况下使用可变参数时要小心。每次调用可变参数方法都会导致数组分配和初始化。

还有一种模式可以让你如愿以偿:

public void foo() { }

public void foo(int a1) { }

public void foo(int a1, int a2) { }

public void foo(int a1, int a2, int a3) { }

public void foo(int a1, int a2, int a3, int... rest) { }

当参数数目超过3个时需要创建数组。

EnumSet类的静态工厂使用这种方法,将创建枚举集合的成本降到最低。


54 返回空的数组或集合,不要返回null

像如下的方法并不罕⻅:

private final List<Cheese> cheesesInStock = ...;

/**
* @return a list containing all of the cheeses in the shop,
* or null if no cheeses are available for purchase.
*/
public List<Cheese> getCheeses() {
	return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}

把没有奶酪(Cheese)可买的情况当做一种特例,这是不合常理的。这样需要在客户端中必须有额外的代码来处理null的返回值:

List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
	System.out.println("Jolly good, just the thing.");

这样做很容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理null返回。


下面是返回可能为空的集合的典型代码。一般情况下,这些都是必须的:

public List<Cheese> getCheeses() {
	return new ArrayList<>(cheesesInStock);
}

如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免多次分配

// Optimization - avoids allocating empty collections
public List<Cheese> getCheeses() {
	return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}

数组的情况与集合的情况相同。永远不要返回null,而是返回⻓度为零的数组。

// Optimization - avoids allocating empty arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
	return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

55 谨慎返回optional

在Java 8之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回null。这两种方式都不完美:

  1. 抛出异常代价很高,因为在创建异常时捕获整个堆栈trace
  2. 返回null有可能抛NullPointerException异常

在Java 8中,还有第三种方法来编写可能无法返回任何值的方法。Optional<T>类表示一个不可变的容器,它可以包含一个非null的T引用,也可以什么都不包含。

不包含任何内容的Optional被称为空(empty)。非空的包含值称的Optional被称为存在(present)

返回Optional的方法比抛出异常的方法更灵活、更容易使用,而且比返回null的方法更不容易出错。

例如在第30条中,有一个根据集合中元素的自然顺序计算集合最大值的方法:

public static <E extends Comparable<E>> E max(Collection<E> c) {
	if (c.isEmpty())
		throw new IllegalArgumentException("Empty collection");
	
	E result = null;
	for (E e : c)
		if (result == null || e.compareTo(result) > 0)
			result = Objects.requireNonNull(e);
	return result;
}

如果给定集合为空,此方法将抛出IllegalArgumentException异常。更好的替代方法是返回Optional<E>。下面是修改后的方法:

public static <E extends Comparable<E>>
	Optional<E> max(Collection<E> c) {
	if (c.isEmpty())
		return Optional.empty();
	
	E result = null;
	for (E e : c)
		if (result == null || e.compareTo(result) > 0)
			result = Objects.requireNonNull(e);
	return Optional.of(result);
}

将null传递给Optional.of(value)是一个编程错误,会抛NullPointerException异常。Optional.ofNullable(value)方法接受一个可能为null的值,如果传入null则返回一个空的Optional


Stream 上的很多终止操作返回Optional。可以用Stream重写max方法,Streammax 操作会为我们生成Optional的工作:

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
	return c.stream().max(Comparator.naturalOrder());
}

如果方法返回一个Optional,则客户端可以选择在方法无法返回值时要采取的操作。有以下两种方式:

1. 指定默认值

String lastWordInLexicon = max(words).orElse("No words...");

2. 抛出任何适当的异常

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

注意,我们传递的是异常工厂,而不是实际的异常。这避免了创建异常的开销


有时候,可能会遇到这样一种情况:获取默认值的代价很高,我们希望避免这种代价。对于这些情况,Optional提供了一个方法orElseGet,传入一个Supplier<T>


Optional还提供了isPresent()方法,可以将其视为安全阀。如果Optional包含值,则返回true;否则返回false。


当使用Stream时,经常会遇到Stream<Optional<T>>,为了推动进程还需要一个包含了非空optional中所有元素的Stream<T>。Java 8里可以这样写:

streamOfOptionals
	.filter(Optional::isPresent)
	.map(Optional::get)

并不是所有的返回类型都能从Optional的处理中获益。容器类型,包括集合、映射、Stream、数组和Optional,不应该封装在Optional中。与其返回一个空的Optional<List<T>>,不还如返回一个空的List<T>


那么什么时候应该声明一个方法来返回Optional<T> 而不是T 呢?

如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回Optional <T>的方法


使用Optional还有一些其他的注意事项:

  1. 永远不应该返回基本包装类型的Optional(小型的BooleanByteCharacterShortFloat 除外)

  2. 不适合将optional作为键、值、集合或数组中的元素

  3. 除了作为返回值之外,不要在任何其他地方中使用Optional


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

如果API要可用,就必须对其编写文档化。


要正确地记录API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释。如果一个类是可序列化的,应该对它的序列化形式编写文档。puiblic类不应该使用无参构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为所有没有被导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出API元素那样完整。


方法的文档注释应该简洁地描述方法与其客户端之间的约定。这个约定应该说明方法做了什么,而不是它如何完成工作的。文档注释应该列举方法的所有前置条件以及后置条件

前置条件:为了使客户能够调用这个方法,必须要满足的条件
后置条件:调用完成之后,哪些条件必须满足

通常,每个未受检的异常都对应一个前提违例(precondition violation),要在受影响的参数的 @param 标签中指定前置条件


方法还应在文档中记录它的副作用(side effort)。例如,如果方法启动后台线程,则应该在文档里说明。


文档注释应该为每个参数都有一个 @param 标签,一个 @return 标签,以及一个 @throw 标签。


@param@return 标签后面的文本应该是一个名词短语,描述参数或返回值所表示的值。@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);

在此文档注释中使用了HTML标记(<p><i>)。Javadoc实用工具将文档注释转换为HTML。


@throw子句中的代码片段周围使用Javadoc的{@code}标签。它使代码片段以 code font(代码字体)形式呈现


@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() { ... }

包含HTML元字符的文档,例如小于号(<),大于号(>)和 and 符号(&),是用{@literal}标签将它们包围起来:

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

文档注释在源代码和生成的文档中都应该是易读的


每个文档注释的第一个「句子」是注释所在元素的概要描述。同一个类或接口中的两个成员或构造方法不应具有相同的概要描述


注意概要描述是否包含句点,例如以「A college degree, such as B.S., M.S. or Ph.D.」会导致概要描述为「A college degree, such as B.S., M.S」。缩写「M.S.」中的第二个句号后面跟着一个空格。最好的解决方案是用{@literal}标签

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

概要描述应该是一个动词短语,描述了该方法执行的操作。例如:

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

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

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

为泛型类型或方法写文档时,请务必记录所有类型参数:

/**
* 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();
}

无论类或静态方法是否线程安全,都应该在文档中描述其线程安全级别


Javadoc具有「继承(inherit)」方法注释的能力。如果API元素没有文档注释,Javadoc将搜索最适用的文档注释,接口文档注释优先于超类文档注释。


对于由多个相互关联的类组成的复杂API,通常需要用描述API总体架构的外部文档来补充文档注释。如
果存在这样的文档,相关的类或包文档注释应该包含到外部文档的链接。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多