检查方法中参数的有效性
请在编写方法时考虑参数的有效性,一般的需要在方法体的开头校验参数的有效性。例如ArrayList的get方法中参数i是一个非负数,如果输入一个负数将会报错:ArrayIndexOutOfBoundsException,这是因为在get方法体首先对i做了检查:
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
如果小于0或者大于数组长度将会抛出异常,这样做的好处是避免错误的参数在被使用时抛出的另类异常。
公共方法需要在JavaDoc中增加@Throws 声明抛出哪些异常。
But!但是也有一些例外的情况,有些时候检查参数有效性代价昂贵,在方法体内已经对参数进行有效性检查,这时候再检查参数有效性显然是做无用功。例如Collections.sort(List)排序之前需要判断list中的元素是否类型相同,才作比较,但是这个操作是在sort方法内部已经实现过了,因此不必再重复检查参数有效性。
必要时进行保护性拷贝
请观察下面的代码有什么地方不妥?
public final class Period {
private final Date start;
private final Date end;
public Period(Date start,Date end){
if(start.compareTo(end)>1){
throw new IllegalArgumentException(start+"after"+end);
}
this.start = start;
this.end = end;
}
public Date getStart() {
return start;
}
public Date getEnd() {
return end;
}
}
乍一看,构造器方法复合上一条说的需要检查参数的有效性,并且这个类是不可变的。但是成员变量Date类型是可变的,客户端可以通过修改Date来攻击该类,下面这段客户端的代码可以改变Period中的start的值。
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
System.out.println(period.getStart());//2019-1-1 14:22:35
period.getStart().setYear(1900);
System.out.println(period.getStart());//2020-1-1 14:22:35
}
为了保护Period实例的内部信息免遭攻击,需要对构造器进行保护性拷贝,以及确保客户端不能改变该类的内部信息。
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());
}
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
}
总结:如果客户端能从类实例中获得可变的成员变量,就需要对该变量进行保护性拷贝,如果变量拷贝的成本大,就需要确保该变量不会被客户端修改,并且在javadoc中注明该变量是不得修改的。
谨慎设计方法签名
谨慎地选择方法名称
方法名称要能体现该方法的作用是什么。还要注意一些框架中特定的命名规范,例如,struts2的action类中的方法不能命名为getXXX方法,因为struts2中对get方法是用来注入成员变量用的。
不要过于追求提供便利的方法
每个方法都应该尽其所能,方法太多会难以记住。
避免过长的参数列表
参数过长不利于我们记住方法的调用,可以将参数封装在一个对象中。
对于参数类型,要优先使用接口而不是类
例如Map<String,String> map = new HashMap<String,String>();绝对不要使用HashMap作为参数类型,这样会限定实例化类型只能为参数类型。
慎用重载
考虑下面的代码,它将根据传入的参数类型List、Set、Collection来进行分类。
public class CollectionClassifier {
public static String classfy(Set<?> s){
return "set";
}
public static String classfy(List<?> list){
return "list";
}
public static String classfy(Collection<?> c){
return "unkonwn Collection";
}
public static void main(String[] args) {
Collection<?> collecton[] = {new HashSet<String>(),new ArrayList<String>(),new HashMap<String,String>().values()};
for (Collection<?> c : collecton) {
System.out.println(classfy(c));
}
}
}
我们期望会打印set、list、unkonwn Collection。但是打印为3个unkonwn Collection!因为选择哪一种重载方法是在编译时已经决定了,所以参数类型都是Collection<?>,打印的都是unknown Collection。
对于重载方法的选择是静态的,即在编译时才确定用哪一个重载方法。
对于覆盖方法的选择是动态的,即在代码运行时根据实例化子类对象的类型来确定覆盖方法的。
观察下面的例子,它将list、set中增加值,然后删除非负数:
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
Set<Integer> set = new TreeSet<Integer>();
for (int i = -3; i < 3; i++) {
list.add(i);
set.add(i);
}
System.out.println(list.toString());
System.out.println(set.toString());
for (int i = 0; i < 3; i++) {
list.remove(i);
set.remove(i);
}
System.out.println(list.toString());
System.out.println(set.toString());
}
我们期望它打印的结果是list:-3,-2,-1.set:-3,-2,-1
但是结果确实list:-2,0,2.set:-3,-2,-1.
这是因为list.remove()方法有两个重载方法remove(int index)和remove(E),我们期望使用remove(E)的方法,但是结果却是用remove(ine index)。
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
将上面的list的删除操作用下面的替换之,则达到期望的结果:
list.remove((Integer)i);
慎用可变参数
可变参数:方法参数中用‘...’来表示某一种参数类型数量可变。
返回零长度的数组或者集合,而不是null值
程序员往往会忘记判断返回值的非空性,而且这种操作是完全可以避免的,所以方法返回零长度的数组或者集合,那么就不要再客户端代码中做额外的处理。