《Effective Java》读书笔记 - 7.方法

时间:2023-03-09 19:35:14
《Effective Java》读书笔记 - 7.方法

Chapter 7 Methods

Item 38: Check parameters for validity

直接举例吧:

/**
* ...其他的被我省略了
* @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
}

这里记得要写文档。

对于那些unexported method(也就是自己内部用,而不公开到API里的方法),一般是用assertions,比如:

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

想让他们有效,需要指定-ea (or -enableassertions) flag to the java interpreter。

Item 39: Make defensive copies when needed

主要是如果你的类有指向mutable对象的field的话,要进行defensive copy,举个例子,假设下面这个类中的date这个field是“内部实现”:

public final class Example{
private final Date date;
public Example(Date date,) {
this.date = date;
}
public Date getDate() {
return date;
}

Java中的Date是mutable的,所以很容易破坏这个类的内部field:

Date date = new Date();
Example example = new Example(date);
date.setYear(78);

所以我们要把constructor改成:

public Example(Date date,) {
this.date= new Date(date.getTime());
//对date这个field需要满足的条件进行验证
if (this.date...) {throw ...}
}

为什么要先defensive copy,再进行验证?因为如果先验证再copy的话,可能中间会被别的线程修改。

注意这里不能用Date的clone方法,因为Date是非final的,所以clone方法返回的可能是一个恶意的子类对象。

接着,getter方法也需要修改,因为很容易通过getter破坏内部field:

Date date = example.getDate();date.setYear(78);

所以需要这么修改,返回一个自己private field的copy:

public Date getDate() {
return new Date(date.getTime());
}

注意这里我们是可以用clone的,因为刚才由于我们对constructor的修改,内部的date field已经可以保证绝对就是一个Date对象,而不是什么Date子类对象。

其实这个例子告诉我们的真正教训是:最好用immutable的对象作为component。

由于defensive copy导致额外的性能损失,有的时候如果能保证调用方是可信的,比如

这个被调用的class和调用方都在同一个package,那么可以不用defensive copy,但是必须在文档中说明“谁都不能改这个对象”。

Item 40: Design method signatures carefully

本条item主要是讲你在设计API时候需要注意的事项:

一.Choose method names carefully。遵守标准的命名约定,以及同一个package中命名的一致性。

二.不要追求提供convenience method。因为这意味着更多的文档,维护,测试。只有真正需要时,再提供。如果不确定,就别提供。

三.不要定义过多参数,最好四个或四个以下。有一些技巧,比如将方法拆分成几个不同的方法,或者创建(inner)helper classes,或者使用item2介绍的Builder pattern。

四.For parameter types, favor interfaces over classes。

五.Prefer two-element enum types to boolean parameters。这是为了可读性和可维护性考虑的。

Item 41: Use overloading judiciously

对于重载方法的选择,是发生在编译时的。比如如果有两个重载方法,唯一不同的是接收参数的类型分别是Set<?>Collection<?>,然后你定义了一个变量:

Collection<?> c = new HashSet<String>()

然后把c传给刚才的重载方法里去的时候,由于编译时c的静态类型是Collection<?>,所以会选Collection<?>的版本,而不会选Set<?>的版本。

所以在API中尽量不要设计让人感到疑惑的方法重载,也就是让调用方无法确定哪个方法会被调用,保守的做法是尽量不要定义参数数量相同的重载方法,因为最多不过就是改个名字的事儿,比如ObjectOutputStream中的writeBoolean(boolean), writeInt(int), 和writeLong(long)。或者说如果两个类“完全没关系”,也就是neither one is an instance of the other one,那么重载也是安全的。如果不遵照这种简单的做法,编译器如何做出选择的规则是十分复杂的(我记得在看C# in depth的时候就是),在Java language specification中占了非常多篇幅,而很少有人真得理解这些规则。

有的时候如果两个重载方法做的事实际上一模一样,那么你可以违背上面说的简单规则,比如String中有两个重载方法分别接受StringBuffer和CharSequence类型的参数(CharSequence是被StringBuffer, StringBuilder, String, CharBuffer等类实现的接口),想让这两个方法的行为完全一致,只要让“范围小的”去调用“范围更大的那个”就行了:

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

所以我们完全不在意编译器会选择哪个方法,因为反正最后的行为都是一样的。当然,Java库中也有一些违背上述简单原则的重载方法,可能会造成困惑,所以尽量避免。

下面举个造成困惑的例子:

Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
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);

这里有个很贱的陷阱,其中两个remove的签名分别是Set.remove(Object o)和List.remove(int index),所以Set的remove是会移除某个元素,而List的remove是会移除某个位置的元素。这其中的历史原因是因为在Java引进自动装箱之前,List的remove(int index)和remove(Object o)是不可能导致困惑的,因为int和Object基本就是完全不搭嘎的两个玩意儿,而引入自动装箱后,List.remove方法“遭到破坏”。要得到期望的结果,需要改成list.remove((Integer) i)。

Item 42: Use varargs judiciously

你不应该把一个 以一个数组作为最后一个参数的方法 修改成varargs parameter,虽然这么做不会影响到已经存在的client code。为什么呢?举个例子(我自己想的例子):比如你把public static void test(Object[] objs)改成了public static void test(Object... objs),那么你如果int[] ints = {1,2,3};test(ints),就只能是一个“有一个元素的Object[]数组”,这个唯一的元素是一个数组对象{1,2,3},而你实际期待的行为应是:Integer[] ints =...;test(ints),这样的话就是一个“有三个元素的Object[]数组,三个元素分别是1,2,3的Integer对象”了。

所以慎用varargs,只有真正需要的时候再用。如果为了性能考虑(因为创建数组需要开销)你可以用以下方法:

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) { }

这让我想到了Console.WriteLine对各种primitive type都写了重载方法,避免不要的装箱。

Item 43: Return empty arrays or collections, not nulls

先举个反例:

private final List<Cheese> cheesesInStock = ...;
public Cheese[] getCheeses() {
if (cheesesInStock.size() == 0) return null;
...
}

这样会造成client每次都要做额外的null检查,其实是不必要的。所以你应该直接返回一个empty的List。另外,如果要对性能考虑的话,一个长度为0的数组对象肯定是immutable的对吧,那么你可以每次都返回这个相同的空数组对象(如果你需要返回一个空的数组或一个空的集合的时候),比如:

// The right way to return an array from a collection
private final List<Cheese> cheesesInStock = ...;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

List的toArray的文档中说了:你传给它的参数决定了它返回数组的类型,如果你传给它的参数(一个数组)的容量够的话,那就返回你传的这个数组,否则它会自己new一个。所以上面的方法中,如果对应的List是empty的话,每次都会返回同一个数组(EMPTY_CHEESE_ARRAY )。如果你要返回一个Collection的话,可以用比如Collections.emptyList()。

Item 44: Write doc comments for all exposed API elements

你应该用doc comment给每一个要export的class, interface, constructor, method, field declaration都注上文档。你甚至应该为大部分unexported的类写上文档,为了更好的可维护性。

doc方法的时候应该简要地写出这个方法是干嘛的,而不是how it does the job(Item 17中说的专门用来被继承的方法例外),并应该说明参数应该满足的条件(一般是通过@throws这个tag说明),以及造成的各种“side effects”(比如改变了某个对象,或者start了一个background thread)。以及还应该说明thread safety和是否能被序列化。具体来说,对于每一个参数都要写对应的@param tag,除非是void否则就要写@return tag,用@throws说明每一个不管che不check的Exception。根据约定,@param tag@return tag后面都是先跟一个名词,说明这玩意儿代表什么,而且@param, @return, or @throws tag都不以句号结束,请看示例:

/**
* 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标签,但没必要弄得太花俏和复杂。注意{@code} tag的作用是第一让代码高亮,第二别让HTML解释成标签(也就是说你可以随便使用大于小于号,而不需要用HTML的转义字符)。如果有多行代码,请用:<pre>{@ 多行代码}</pre>

By convention,注释中的this这个词儿一般都是指instance method的对象。

如果你懒得用HTML转义字符,那么可以用{@literal} tag,示例如下:

* The triangle inequality is {@literal |x + y| < |x| + |y|}.

大于小于号随便写。这个tag和{@code}类似,只是不提供代码高亮功能。作者说用这个是为了让注释文档在源代码中也有很好的可读性,我非常同意。

注意每一个doc comment的第一句话是一句概括性的说明(summary description),比如上面的“Returns the element at the specified position in this list.”。同一个类中的两个或多个constructors,以及重载的方法,都不应该有相同的summary description。另外,在这个summary description中以“句号后面跟一个空格”作为结束符(我估计就是显示成HTML的时候只显示前面这么多作为总的概括说明),所以你需要把并不是作为结束符的某些字符用{@literal }括起来,如{@literal M.S.}。对于方法来说,这句summary description一般以动词开头,对于class和Interface以一个名词开头。

如果有泛型参数的话,记得也要描述一下,比如:

* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values

对于enum类型,每个constant都要写注释,书上是写在对应的constant的上面。

对于annotation类型,它的每一个成员也都要写注释。

{@inheritDoc}可以复用doc comments,但是不在本书讨论范围内。

IDE有插件可以检查你对这些doc comments规则的的遵守。