本章主要介绍一些方法级别的技巧和需要注意的问题。
关于方法体的第一个原则就是:应该在错误发生之后尽快检测出错误 如果不是这样,那么当错误发生时,很难追踪错误的根源。 本条建议就是遵循这个原则,在你的方法体开头立刻检查入参的有效性,抛出含义明确的异常,并且在Java doc文档中指出入参的限制条件以及异常抛出的原因。 如果没有这么做,那么你的方法有可能会失败,更糟糕的是,你的方法不会失败,会返回错误的值,错误的值在另一个地方造成失败。 ### 私有方法的处理 私有方法并未导出给其他人使用,作为方法的创建者和使用者,你自己能保证入参的正确性,作者的建议是使用断言,即assert关键字。 原因是:assert关键字只有在启动时传给解释器-ea这个参数才会生效,这样你自己测试的时候开启断言,用户使用的时候关闭断言,保证了最小的性能开销。 事实上,现在看到的更多的做法是,压根不检查入参,只要你保证为调用这个私有方法的方法编写了单元测试,并验证了私有方法不会在所有情况下传入非法入参,不检查入参也是可以接受的。 你甚至可以为私有的方法编写单元测试,当然,这需要额外的单元测试框架的支持。 另外,在一些检查开销很贵或者不切实际的时候,也可以不必检查入参。有时候,像Collections.sort()
这样的方法就隐式地为我们检查了入参,因此,也不必再额外地重复检查。
联想第十五条,可能会比较容易理解本条。 本条主要讲的是一个提高代码安全性的技巧,即保护性拷贝。 看下面的例子:
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;
}
// Repaired constructor - makes defensive copies of parameters - Page 185
// Stops first attack
// 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(start +" after "+ end);
// }
public Date start() {
return start;
}
public Date end() {
return end;
}
// Repaired accessors - make defensive copies of internal fields - Page 186
// Stops second attack
// public Date start() {
// return new Date(start.getTime());
// }
//
// public Date end() {
// return new Date(end.getTime());
// }
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
忽略注释的部分,可以看到,这是一个表示时间范围的类,表面上看起来,域是final的,而且没有提供Setter方法,这应该是一个不可变的类。 在构造器中,我们有一个入参检查:结束时间不能大于开始时间 然而,由于这个类的两个域都是Date,而Date实际上是一个可变类,因此,外部仍然可以通过下述方法,攻击该不可变类,使其实例在创建后被改变:
public class Attack {
public static void main(String[] args) {
// Attack the internals of a Period instance - Page 185
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
System.out.println(p);
// Second attack on the internals of a Period instance - Page 186
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
}
}
要想真正让这个类成为不可变类,只有使用保护性拷贝的方法,在构造器中将对象拷贝一份存在内部的域上,而在Getter方法中也使用拷贝返回给外部用户,即使外部用户改变,也只改变的是拷贝而已。 例子见上面的注释部分。
下面有一些规则或者需要注意的地方: ### 谨慎选择方法的名称 首先要易懂,其次要保持同一个包相同的命名风格,最后应该选择大众认可的名称。 ### 不要过于追求提供便利的方法 每个方法都应该尽其所能。方法太多会使得类难以学习、使用、文档化、测试和维护。对于接口而言,方法太多会加重接口实现者的负担。只有某一项操作经常被用到的时候,才考虑为它提供快捷方式,如果不能确定,还是不提供的好。(注:本文作者不能赞同书中的这种观点,《代码简洁之道》这本书告诉我们,方法应该尽可能地只干一件事,如果很多读者看到了这一条,会误以为作者宣扬把大量的逻辑都塞进同一个方法里,而不是分拆开来,这与一个方法只干一件事的原则违背。事实上,方法既不能什么都想干而变的臃肿,也不能拆分的太凌乱太琐碎。有一个度在里面。通常,多次被使用到的逻辑毫无疑问应该抽出来。仅使用一次但难以理解的逻辑也要抽出方法来,这样可以通过方法名来阐释逻辑) ### 避免过长的参数列表 四个参数是极限。超过四个参数,你就应该考虑是不是应该使用什么办法来降低参数数量。参数不能太多的原因有以下几个: 1. 没人能记住你的方法的参数列表,除非到声明的地方去看,现代IDE可以让你不用点进去也能看,但是那又怎样?你还是会用错,尤其参数多达10个以上时,IDE的提示框都变的很难看 2. 超长的参数列表意味着有可能要超多的参数有效性检查,参考第三十八条,当别人看你的方法的时候,前二十行都是该死的有效性检查,对阅读者毫无意义。 3. 很多个相同类型的参数放在相邻的位置简直是噩梦,不小心搞反了编译器也不会报错,但是运行时会出现难以觉察的错误。
有几种方法可以降低参数列表的数目: 1. 将方法分解,每个子方法只包含一部分参数。(如果你成功使用这种方法降低了参数列表,那么说明原来的方法根本就是糟糕的设计,记得吗?方法应该尽可能地只干一件事,如果它被成功拆分了,那么极有可能是它太贪心了,干了很多事) 2. 使用辅助类来将参数打包,通常使用静态成员类来做,当你发现某一些参数总是成双成对,成群结队出现的时候,说明有一个类应该出现。 3. 应该使用Builder设计模式(见第二条),不但使用一个类把参数包装起来,还允许用户逐步地设置参数,并最终执行方法。
不光是参数列表,在任何地方都应该优先考虑使用接口而不是类,否则未来的某一天,你可能会后悔,发现某个方法非常满足你的要求,然而很遗憾它有一个入参问你要HashMap,可你只有一个TreeMap,怎么办?如果它问你要的是Map接口,该多么美好啊。 ### 对于boolean参数,要优先使用带有两个元素的枚举类型 这种说法我还是第一次见,作者希望你在传入true或者false这种值的时候,考虑使用枚举类型替代,枚举类型标明true和false真正代表的东西。 比如boolean参数的名字叫做isApple
,是苹果吗?如果是,你做一个逻辑,如果不是,你就做另一个逻辑。这时你可以用含有苹果和香蕉的枚举类型代替这个boolean参数。 这样做的目的是,当未来你的代码又多了一种水果,比如梨,你只需要把梨加入枚举类里就行了,如果你为了兼容梨再加一个参数,那么所有使用这个方法的地方都要改,当然你也可以再写一个方法,那也是个坏主意,因为新方法大多数逻辑都和旧方法很像。