0%

Effective Java 第三版 (7) 方法

方法

§ 检查参数的有效性

公有方法,要用 Javadoc 的 @throws 说明违反参数限制时的异常,如 IllegalArgumentExceptionNullPointerException 等。

非公有方法,通常应该使用断言来检查它们的参数。断言如果失败,会抛出 AssertionError

1
2
assert a != null;
assert offset >= 0 && offset <= a.length;

必要时进行保护性拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Period {
private final Date start;
private final Date end;

public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException();
}
this.start = start;
this.end = end;
}
public Date getStart() {
return start;
}

public Date getEnd() {
return end;
}
...
}

如上所示,start 与 end 虽然使用了 final,并且进行了参数约束,但是由于 Date 类本身是可变的。所以很容易违反这个约束:

1
2
3
4
5
6
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);

// 修改
end.setYear(78)

从 Java 8 开始,解决此问题的方法是使用 Instant(或 LocalDateTimeZonedDateTime)代替 Date,因为 Instant 和其他 java.time 包下的类是不可变的。

Date 已过时,不应在新代码中使用。

为了保护 Period 实例的内部信息避免受到这种攻击,对构造器的每个可变参数进行保护性拷贝是必要的。修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
}

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

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

并且,对于参数类型可以被不可信任方子类化的参数,不要使用 clone 方法进行保护性拷贝。

§ 谨慎的设计方法签名

  • 仔细选择方法名名称
  • 不要过分地提供方便的方法。每种方法都应该“尽其所能”
  • 避免过长的参数列表
  • 对于参数类型,优先选择接口而不是类
  • 考虑使用两个元素枚举类型代替布尔型,除非布尔型参数的含义在方法名中是明确的。枚举类型使代码更容易阅读和编写,还可以方便地在以后添加更多选项。

§ 谨慎的使用重载

以下代码,当我们使用 HashSetArrayListHashMap 调用 classify 方法时,会打印 Unknown Collection 三次:

1
2
3
4
5
6
7
8
9
10
11
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";
}
}

因为,要调用哪个重载,是在 编译时 做出的决定。对于 HashSetArrayListHashMap,参数编译时类型都是相同的:Collection<?>

对于重载方法的选择是静态的,对于被覆盖的方法选择是动态的。

§ 谨慎的使用可变参数

Java 1.5 开始增加了可变参数。

1
2
3
4
5
6
7
static int sum (int... args) {
int sum = 0;
for (int arg : args) {
sum += arg;
}
return sum;
}

这个对于方法接受 一个或多个参数 是合适的。但是对于 零或更多 是不合适的。

即使是在代码中进行检查,但是这样是运行时失败,而不是编译时失败。

通常可以提供两个参数解决:

1
static int sum (int firstArg, int... args) {

§ 返回零长度的数组或者集合,而不是 null

永远不要返回 null 来代替空数组或集合,通常客户端都需要进行 null 来进行检查,很容易出错。

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

如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,但是请记住,这是一个优化,很少需要它:

1
2
Collections.emptyList()
Collections.emptyMap()

永远不要返回 null,而是返回长度为零的数组:

1
2
3
4
5
private static final Cheese[] EMPTY = new Cheese[0];

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

§ 谨慎的返回 Optional

在 Java 8 之前,编写特定情况下无法返回任何值的方法时,要么抛出异常,要么返回 null(假设返回类型是对象或引用类型)。这两种方法都不完美。(抛出异常代价很高,因为在 创建异常时会捕获整个堆栈跟踪)

Optional<T> 表示一个不可变的容器,它可以包含一个非 null 的 T 引用,也可以什么都不包含,不包含任何内容的 Optional 被称为空。

1
2
3
4
5
6
7
8
9
10
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);
}

如何选择返回 Optional 而不是返回 null 或抛出异常呢? Optional在本质上类似于检查异常。

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

1
2
3
4
String lastWordInLexicon = max(words).orElse("No words...");

// 传递的是异常工厂,而不是实际的异常,避免了创建异常的开销,除非它真的被实际抛出
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

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

  • Optional 是必须分配和初始化的对象,从 Optional 中读取值需要额外的迂回。 这使得 Optional 不适合在某些性能关键的情况下使用
  • 永远不应该返回装箱的基本类型的 Optional。而考虑使用 OptionalIntOptionalLong 等。

大多数其他 Optional 的用法都是可疑的。例如,永远不要将 Optional 用作映射值等等。

§ 为公开的API编写文档注释

  • 文档注释在源代码和生成的文档中都应该是可读的通用原则
  • 类或接口中的两个成员或构造方法不应具有相同的概要描述
  • 记录泛型类型或方法时,请务必记录所有类型参数
  • 在记录枚举类型时,一定要记录常量,以及类型和任何公共方法
  • 在为注解类型记录文档时,一定要记录任何成员
  • 无论类或静态方法是否线 程安全,都应该在文档中描述其线程安全级别