本文结合《Effective Java》第七章《方法》和自己的理解及实践,讲解了设计Java方法的优秀指导原则,文章发布于专栏Effective Java,欢迎读者订阅。
清单1: 检查参数的有效性
在每个方法的开头检查方法的参数,遵循“应该在发生错误之后尽快检测出错误”这一原则。
对于公有的方法,对于校验失败的入参,抛出异常,常见的有IllegalArgumentException(非法参数异常)、Arithmeticexception(运算条件异常)等,并在Javadoc里进行说明。
对于私有方法,不像public方法需要防范外界的不可信任性,private方法是给创建者自己使用的,因此遵循的是一种契约关系,因此对于参数的校验,可以使用assert断言。使用断言的好处是,在开发阶段,可以使用-ea来开启断言,保证调用者入参的准确性,在生产环境,可以禁用断言,去掉参数检查,提高性能。关于java assert的更多信息,可以参考这篇博客,断言绝对不是鸡肋
当然,有时候,有效性检查已经在方法后续的执行过程中完成,比如Collection.sort方法,会在排序中校验对象是不是可以互相比较,那么就不必在调用sort方法之前进行检查了。
清单2: 必要时进行保护性拷贝
我们设计的方法,往往会在不经意间,给外界提供修改对象内部状态的机会,破坏了类的不可变性(不可变性,参考专栏的另一篇文章Java 设计类和接口的八条优秀实践清单——清单3 使类的可变性最小化)。
比如下面这个类,它声称可以表示一段不可变的时间周期:
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;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
虽然这个类给Date对象声明了final,但如同专栏之前说的, final只是引用不可变,客户端很容易去修改start和end:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);
同时由于类提供了返回start和end的方法,客户端还能这样修改:
Date start = new Date();
Date end = new Date();
p = new Period(start, end);
p.end().setYear(78);
解决这个问题的关键在于进行保护性拷贝,给客户端返回一个新的对象,使得客户端在修改它获取到的对象时,不会影响原来的对象,加入保护性拷贝后的类如下:
public final class Period {
private final Date start;
private final Date end;
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 new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
public String toString() {
return start + " - " + end;
}
}
修改后的类,在构造器中,不直接使用外部传入的对象,而是进行一次拷贝;同时在每个返回内部属性的方法,不直接返回原有对象,也做一次拷贝,这样,这个类才是真正的不可变类。
这里的构造器,为什么要先拷贝再校验呢?不能这样写吗:
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
原因是,如果这样写,就会有一个漏洞——在执行完校验和进行拷贝的这段时间,外部传入的start和end对象有可能被修改,而不再满足我们的校验条件。这种在计算机安全术语中,叫做Time-Of-Check/Time-Of-Use或者TOCTOU攻击。
到这里,我们终于把这个类做到不可变了,但我们回头想想,如果一开始我们不使用Date对象,而是直接使用Date.getTime()的long基本类型来表示的时间,不就不需要保护性拷贝了吗?由此得到另一个启示——尽量使用不可变对象作为对象内部的组件。
清单3: 避免过长的参数列表
好的方法,参数不能超过4个,超过4个,方法就不方便使用。
有三种方法可以缩短过长的参数列表:
1、把方法分解成多个独立功能的方法
很多时候,我们的方法参数过多,是因为实现的功能太复杂。比如,java的List并没有提供“在子列表中查找元素第一个索引和最后一个索引”的方法,如果提供,那么方法就需要三个参数:子列表的开始索引、子列表的结束索引、要查找的元素;List方法提供了subList、indexOf和lastIndexOf方法,客户端通过组合使用这三个防范,就能实现这个功能。
2、使用辅助类,存储原来的参数,方法入参改为辅助类即可
3、使用builder模式,参考专栏的另一篇文章 Java创建对象的方法清单 —— 原来还可以这样创建对象
清单4: 返回0长度的数组或者集合,而不是null
如果一个方法声明返回的是一个数组或者集合,那么当你打算返回null时,请返回一个长度为0的数组或者集合,这样能让调用者省去null对象校验。
有人可能会担心每次都创建一个空的数组或者集合去返回会影响效率,那么完全可以先创建好不可变的空的数组或者集合。
对于数组,只需要加上final修饰符,那么,零长度的数组就是不可变的。
private static final String[] EMPTY_STR_ARRAY = new String[0];
对于集合,Collections类提供了emptyList、emptySet、emptyMap方法,同样可以返回一个不可变的空集合。
清单5: 为每个方法编写Javadoc文档注释
工作这段经历,让我深刻地体会到,没有注释,读懂代码很辛苦;良好且正确的注释,可以帮助更快更好地去读懂代码。
一个方法的注释应该包含以下几个部分:
方法概要描述:往往是一个简单的动词
方法使用后产生的一些副作用等详细描述:比如是否会启动了线程,是不是线程安全等
方法入参描述:使用@param
方法抛出的异常描述: 往往要在异常后面写上什么情况下会抛出
示例:
/**
* Returns the element at the specified position in this list.
*
* This method is thread-safe, and it will start a thread.
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* (<tt>index < 0 || index >= size()</tt>)
*/
关于Java注释的更多指导,可以参考Oracle最权威的指导文档 How to Write Doc Comments for the Javadoc Tool
以上,希望能对你设计方法有所帮助。