第八章:方法
检查方法参数
failure atomicity(原子性失败): 失败的方法调用,应该使对象保持在被调用之前的状态。
如果一个方法在执行之前未检查参数,那么可能出现以下几种情况:
- 方法执行失败,抛出令人困惑的异常。
- 方法执行成功,但是返回的值不正确。
- 方法执行成功,返回值也是正确的,但是有些对象的状态可能处于未知的状态,可能在未知的时刻,未知的调用情况下出现错误。
以上几点都违反了原子性失败,基于以上几点,所以在方法执行前一定要进行方法参数的检查。检查遵循如下约定:
对于
public
和protected
方法,可以在方法上使用@throw
注释说明方法参数可能会引发何种异常,比如IllegalArgumentException, IndexOutOfBoundsException, NullPointerException
,并且在参数未通过检查时,抛出这些异常。使用
@Nullable
和其他相似的注解,注解在方法参数上,指示方法参数是否可以为空等等。使用
Objects
工具类中提供的静态方法,进行方法参数的校验。对于不被暴露对外的方法(
private
),可以控制在什么情况下被调用,可以确认什么时候被调用,什么样参数会被传入,可以使用断言assert
,通常在测试时候可以使用-ea
开启断言,在正式环境默认是不开启。对于有些参数在方法本身没有用到,只是保存起来以便以后使用,对于这种参数检查特别重要,因为如果不检查在之后使用中出现了错误,你无法知道是参数错误还是其他错误,举例:静态工厂方法
Lists.asList()
,构造函数等。参数检查也有例外,比如检查参数非常的消耗资源,或者有效性检查,隐式的在后续计算中完成,
Collections.sort(list)
(不用检查list
中每个元素是否可以比较,sort
方法会做出检查),当然有可能隐式的检查抛出的异常和我们预期的异常是不同的,所以此时我们需要进行异常转换。并非参数检查越多越好,如果一个方法能接受所有的参数而且都能工作,非常的通用,那么对参数的限制当然是越少越好。
参数的检查应该写在方法的注释中。
防御性的复制(拷贝)
不可变类
1 | // Broken "immutable" time period class |
上述代码看起来是不可变,实际上有两个问题:
start()
,end()
方法返回了类的成员变量的引用,调用者可以使用该引用改变成员变量。构造函数中将传入参数的引用赋给成员变量,如果该引用发生了变化,那么该类的实例也发生了变化。
修改:
在构造函数中使用形参内容创建一个copy的中间对象,然后在将中间对象赋值给成员变量,这样即使传入的参数内容发生了改变也不在会影响该类的实例。
1
2
3
4
5
6
7public 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);
}start()
,end()
方法返回该成员变量的copy对象。
1 | public Date start() { |
ps: 在
jdk 8
以后的版本可以直接使用java.time
下的LocalDate
,因为LocalDate
被设计成不变的。但是本例讨论的是不可变类在内部有可变成员变量时候如何保证不可变性。
defensive copy
: 以上设计就是一种防御性复制(拷贝)。
一个不可变的类中含有可变的成员变量,在构造函数中进行防御性复制是不可缺少的。
防御性拷贝应该是在参数检查之前进行,并且参数检查应该检查防御性拷贝之后对象,如果先进行参数检查,在进行拷贝,那么在参数检查之后拷贝进行之前这段脆弱的窗口期其他线程就可以改变参数的内容(虽然时间很短,但是多线程可能出现这种情况),那么在进行拷贝时,可能就是一个不正确的值。在计算机安全社区,这种是一种攻击手段,叫做:
TOCTOU : tiem of check/time of use
上述构造方法中并没有使用date.clone()
方法获得一个拷贝,因为Date
类可以被继承,所以实际的参数就可以是Date
的子类的实例,在这种情况调用clone
方法,并不能保证正确性和安全性。所以对于 不被信任 可以被子类化的参数(参数的类是可以被继承,实现(implements))不要使用其clone()
方法。但是在start()
,end()
成员方法时可以使用clone()
方法,因为,此时我们已经知道了start
,end
成员变量就是Date
类型,是可以被信任的。
可变类
在编写可变类时候也要仔细的考虑是否可以接受客户端传入一个可变的参数,如果不能,那么就要进项防御性拷贝,避免客户端改变参数,影响该类的实例。
其次在返回类的成员变量之前也要考虑是否可以接受客户端改变该类的成员变量的引用,或者改变其内容,如果不能,那么也要进行防御性拷贝。
如果使用不可变类的对象作为成员变量,那么就不必考虑防御性拷贝。
简而言之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须考虑是否要进行防御性拷贝。
仔细的设计方法签名
仔细的设计方法名称,应该是见名之意,容易理解,并且保持和类的内部或者包内部其他方法名称格式保持一致,而且方法名也不能过长。
不要过于追求便利:每个方法都应该做到它的责任,太多的方法,使类或者接口难以维护,阅读,实现。
避免过长的参数列表: 最多4个参数,特别使相同类型的长参数列表,调用的时候记不住顺序,类型又相同,非常容易传错参数,但是类型相同,编译又不会出错。如果参数太多,考虑查拆分为多个方法; 或者写一个辅助类,保存这些参数; 或者使用
builder
构造模式。使用接口类型作为参数类型:增加通用性。
使用枚举代替boolean参数。
谨慎的进行方法重载
1 | // Broken! - What does this program print? |
灵魂拷问:输出什么? 结果是3次”Unknow Collection”,因为在编译期间所有参数都是c.要调用哪个方法是在编译期间决定的,所以尽管实际参数类型是set
,list
也还是调用classify(Collection<?>)
调用。
重载方法是静态选择的或者说是编译期间选择的,而重写方法的选择是动态的或者说是在运行期间的,选择依据是重写方法在运行时候被调用传入的参数类型。比如如下代码:
1 | class Wine { |
如上代码就会输出我们想要的结果:wine, sparkling wine, champagne
结论:对于具有相同数目参数的方法来说,应该尽量避免重载
谨慎的使用可变参数
返回零长度的数组或者空集合而不是返回null
返回null
值,调用的客户端必须要做null
检查,实际上0长度的数组或者集合和null
代表了一个意思,都是没有的意思,显然返回0长度的集合或者数组更加合适。
慎用optionals
Optional
是java 8
提供的一种容器,用来解决npe
问题。实际上类似另一种checked exception
操作,只是该操作不像抛出异常时需要爬栈(实际自定义异常也可以关闭爬栈),比较优雅。
容器类型,比如collections, maps, streams, arrays
和optioal
自身(也可看做一个容器,只能存放至多一个元素)不应该使用optional
进行包装。
如果一个方法可能返回null
可能返回具体的值,且客户端要对没返回值进行特殊处理,那么因该定义返回Optional<T>
不要使用optional
包装一个原始类型的装箱类型,java
提供了OptionalInt, OptionalLong,OptionalDouble.
3种类来处理这种情况,Boolean, Byte, Character, Short, and Float.
这些没有对应的类,所以最好是不用optional
进行包装,因为会进行2曾包装,所以最好是直接返回原始类型的值。
不要集合中使用optional
,也不要将optinal
作为key
存入Map
.如果这么做了,在检查元素是否在集合中就会很麻烦。得不偿失。
不要将optional
作为成员变量使用。
为所有导出的api元素编写注释
注释应该简洁的说明方法是做什么的,而不是怎么做;同时文档注释中应该包含该方法调用的前置条件和后置条件
@param
: 描述参数,前置条件@return
: 返回值@throws
: 如果不满足前置条件会抛出什么异常,该描述应该包含一个”如果(if)”,表示异常条件什么时候会抛出@code
: 在注释中使用代码时候使用。如
{@code int index = 1; index++;}
@literal
: 如果文档包含html
元字符比如<
,>
可以使用该注释是java doc comment不解释该字符.@link
: 链接到别的类或者方法,字段等.@see
: 同{@link}
区别是需要单独一行,顶头写,不能混在注释里面。