写在前面
《Effective Java》原书国内的翻译只出版到第二版,书籍的编写日期距今已有十年之久。这期间,Java已经更新换代好几次,有些实践经验已经不再适用。去年底,作者结合Java7、8、9的最新特性,编著了第三版(参考https://blog.csdn.net/u014717036/article/details/80588806)。当前只有英文版本,可以在互联网搜索到PDF原书。本读书笔记都是基于原书的理解。
以下是正文部分
函数最佳实践(Methods)
本章包括:
- 实践49 校验参数的有效性(Check parameters for validity)
- 实践50 必要时,使用保护性拷贝(Make defensive copies when needed)
- 实践51 仔细设计函数签名(Design method signatures carefully)
- 实践52 审慎地使用重载(Use overloading judiciously)
- 实践53 审慎地使用varargs(Use varargs judiciously)
- 实践54 返回空集合而不是null(Return empty collections or arrays, not nulls)
- 实践55 审慎地使用 Optional 作为返回(Return optionals judiciously)
- 实践56 为所有外部暴露API编写注释文档(Write doc comments for all exposed APIelements)
实践49 校验参数的有效性(Check parameters for validity)
在编程时,对于函数入参可能会有一些限制,例如参数作为数组下标不能为负数;参数作为对象引用不能为null。最好的实践是注释说明参数限制并在函数入口进行参数检查,越早发现问题越好。
对于声明为private
的函数,由于开发者有完全的控制权和访问与其,在内部使用assert
来校验即可。注意,只有在启动java程序时,指定了-ea
或者-enableassertions
参数,assert语句才回发挥实际用途,在校验失败时抛出AssertionError
。对于声明为public
和protected
的方法使用Javadoc的@throws
注释关键词来注明如果函数调用违背了规则,将抛出的异常。通常,异常的类型为IllegalArgumentException
, IndexOutOfBoundsException,
, NullPointerException
。示例如下:
// Private helper function for a recursive sort
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;
}
/**
* * 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);
}
注意上述代码并没有注明NullPointerException
,这是因为BigInteger
类已经内置了这个问题,如果m
为null
则m.signum()
将抛出NullPointerException
。从Java7开始,使用Objects.requireNonNull
方法可以方便地进行空指针检查。Java9进一步在Object
引入了下标检查checkFromIndexSize
,checkFromToIndex
和 checkIndex
this.strategy = Objects.requireNonNull(strategy, "strategy");
如果检查特别耗时,或者实现上不可行又或者在相关点已经有了检查,这几种情况下可以省略参数校验。
实践50 必要时,使用保护性拷贝(Make defensive copies when needed)
// Broken "immutable" time period class
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;
}
}
构造一个合理的时间段,起始时间保证早于结束时间,初看该函数是没问题的。但是注意由于Java在传递对象时,使用的是引用拷贝,作为参数Date
在外部的修改会影响类内的引用值。典型的攻击手法如下:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
对于这个问题,在Java8中可以使用Instant
或者ZonedDateTime
类替换Date
。推而广之,我们可能在某些API确实需要传入可变对象,但在类内部希望其值不可再修改。这是,就需要用到保护性性拷贝(defensive copy)技术。上面的缺陷类可以这么修改,注意,这两句拷贝必须放在函数最开始。为了防止操作时对象在其他线程被修改,拷贝完,再做参数校验等其他操作。
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
代码中用的是最原始的构造函数来建立新的对象而不是clone
方法。这是因为clone方法不一定返回java.util.Date
原始对象,它如果在子类中被重写,可能在拷贝过程中进行某些污染操作,并返回java.util.Date
的子类对象。
解决了第一种修改"不变量"的攻击,还有第二种等着我们...
// Second attack on the internals of a Period instance
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()); }
实践51 仔细设计函数签名(Design method signatures carefully)
首先,函数名要符合编程规范,并且在一个包内部保持风格一致。不要使用过长的函数名,但同时也要保持它的可阅读性。
不要为了使用方便而抽象出过多的函数。函数太多会增加学习、使用、文档、测试及维护的负担。
避免过长的函数参数列表,尤其是很多参数的类型都相同的时候,容易误用。实践中,建议参数个数尽量少于5个。下面列出三种解决参数列表过长问题的方法
- 把函数重构成多个函数,每个函数只需要传入部分参数进行处理
- 将多个参数视为一个对象,创建辅助静态成员类
- 使用
Builder模式
进行对象创建及函数调用,例如在类的Builder模式
最后是build(),在函数的Builder模式
最后是execute()。这种方法在有可选参数的场景尤其适用。
对于参数类型
- 如果对象是某个接口的实现,则最好用接口类型而不是类类型。例如用
Map
而不是HashMap
。 - 除非意义明确,使用Enum代替Boolean,增加代码可读性。
实践52 审慎地使用重载(Use overloading judiciously)
下面的程序对classify
函数进行了重载,期望能判断集合的类型。从功能角度,程序设计者的一个预期输入应当是 "Set List Unknown Collection" 。可实际上,运行这段程序的输出是 "Unknown Collection Unknown Collection Unknown Collection" 。
// Broken! - What does this program print?
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(),
new ArrayList(),
new HashMap().values()
};
for (Collection> c : collections)
System.out.println(classify(c));
}
}
重载函数的调用是在编译时(compile time)决定的,也就是说对于上述程序,运行时(runtime)调用classify
只有一个选择——classify(Collection>)
。
- 重载函数的选择是静态的,编译时确定
- 重写函数的选择是动态的,运行时确定
classify
可以修改如下:
public static String classify(Collection> c) {
return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}
对于重载,一个安全、保守的建议是:重载函数的参数个数不能相同(如果函数有varargs,那么久不要进行重载)。Java标准库的ObjectOutputStream
给出了很好的示例,对于写操作,它没有去重载write
,而是针对不同的参数类型,给出了writeBoolean(boolean)
,writeInt(int)
,writeLong(long)
等。稍微放宽一点,至少保证参数类型完全不同且无法相互转换。例如ArrayList
的两种构造方法,一个是参数为int,一个是参数为Collection
。这种情况下,使用时基本不会搞混。
另一个误用的示例如下:
class SetList {
public static void main(String[] args) {
Set set = new TreeSet<>();
List 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);
}
}
执行main函数将输出[-3, -2, -1] [-2, 0, 2]
。
实践53 审慎地使用varargs(Use varargs judiciously)
从原理上讲,varargs
会根据入参个数,创建一个相同大小的数组,把所有参数放入数组中,然后再传递到方法里面。因此 varargs
可以直接当数组使用,比较方便,但是同时数组的空间分配和初始化又会引入额外的系统开销,不适用与高性能场景。
static int sum(int... args) {
int sum = 0;
for (int arg : args) sum += arg;
return sum;
}
关于性能问题,一个可能的替代方式是,对于常用场景不使用 varargs
。例如 95% 的情况下,参数不超过3个,那么你应该使用重载。
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) {}
实践54 返回空集合而不是null(Return empty collections or arrays, not nulls)
我们常常看到这样的代码:
/**
* * @return a list containing all of the cheeses in the shop, * or null if no cheeses are
* available for purchase.
*/
public List getCheeses() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
这种返回 null
的做法,是的“空”这个场景被特殊化。所有外部调用点,都需要针对这种空场景进行处理。一旦外部处理不当,可能会在运行时导致程序出错。因此,此时应当返回一个空的集合,而不是 null
。返回空集合并不增加多少实际的系统开销。
List cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
推荐的方式如下:
//The right way to return a possibly empty collection public
List getCheeses() {
return new ArrayList<>(cheesesInStock);
}
//The right way to return a possibly empty array public
Cheese[] getCheeses() {
return cheesesInStock.toArray(new Cheese[0]);
}
如果实在担心性能问题,可以用 static
空集合。Java 标准库中已经集成了 Collections.emptyList
, Collections.emptySet
, Collections.emptyMap
几种。
// Optimization - avoids allocating empty collections
public List getCheeses() {
return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}
// 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 作为返回(Return optionals judiciously)
在Java8中,Optional
是一种不可变的容器类,它包含单个的非null
的 T
对象,或者什么也不包含(nothing at all)。有了 Optional
,我们处理空返回不用像抛出异常那样重型化,也不用专门返回 null
特殊处理。
// Returns maximum value in collection as an Optional
public static > Optional max(Collection 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
的函数。调用者可以做的操作包括:
// 收到空值时指定其他返回值
String lastWordInLexicon = max(words).orElse("No words...");
// 收到空值时抛出异常,注意此处使用函数式编程方法,避免了非空时浪费资源创建异常
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
// 直接取内容,可能抛出 NoSuchElementException
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
另外,Optional
提供 isPresent()
方法来验证是否非空。但是,通常上面几种用法可以避免使用 isPresent()
,并且更加简洁。
虽然 Optional
有上述优点。但是并非所有数据类型都适用于它。还有,相比于返回 null
,Optional
的开销更大。
- 所有集合类都不适用。例如,返回空的List
优于返回空的 Optional
。- >
- 也不要用它来返回一个封装基础类型,例如
Optional
。 - 不要再数组、集合的key,value上用它
实践56 为所有外部暴露API编写注释文档(Write doc comments for all exposed APIelements)
(略)