标签:
给方法的参数加上限制是很常见的,比如参数代表索引时不能为负数、对于某个关键对象引用不能为null,否则会进行一些处理,比如抛出相应的异常信息。
对于这些参数限制,方法的提供者必须在文档中注明,并且在方法开头时检查参数,并在失败时提供明确的信息,即:
detect errors as soon as possible after they occur
这将成为准确定位错误的一大保障。
如果没有做到这一点,最好的情况是方法在处理过程中失败并抛出了莫名其妙的异常,错误的源头变得难以定位,但这是最好的情况。
更差的情况是方法执行通过,没有发生任何错误,只是得出的结果和方法描述完全不符,最后在某个关键的部分看到奇怪的数据时才亡羊补牢。
对于参数违反有效性时使用的异常类,我们通常抛出IllegalArgumentException
,IndexOutOfBoundsException
,NullPointerException
。
而对于违反约束时抛出的异常类型,需要用Javadoc的@throws标签对其进行说明。
另外,并不是所有参数检查都需要做到这种地步。
如果方法或构造器不对外导出,则可以简单使用assert
来保证参数的有效性。
比如java.util.Collections$CopiesList:
private static class CopiesList<E>
extends AbstractList<E>
implements RandomAccess, Serializable{
//...
CopiesList(int n, E e) {
assert n >= 0;
this.n = n;
element = e;
}
//...
}
但是,有效性检查并不都是简单的,这一操作的代价也可能非常大甚至不切实际,于是有些操作直接将参数检查隐含在计算过程中。
比如java.util.Collections
的sort()
方法,列表中的元素当然是都可比较的,而方法并没有在开头检查有效性,而是计算中遇到问题时抛出异常。
与这种方式相比,提前检查有效性的意义确实不大。
但在计算途中检查参数的有效性需要考虑一点,即方法的原子性。
说道参数就不得不说方法重载,首先上一段例子:
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = { new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values() };
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
很常见的笔试题,会输出三次"Unknown Collection",编译时已决定了将调用的方法。
与重载方法的静态选择(为什么当初会这样设计重载? overriding is the norm and overloading is the exception,似乎没有人希望这种做法 )相对的是覆盖方法,overridden method的选择是动态的。
即,选择方法是在运行时进行的,子类用同样的方法签名覆盖了上级类的方法,如果这个方法是实例方法则会在子类的实例上被调用。
比如下面这个例子,实例的编译时类型对其没有造成影响:
class Wine {
String name() {
return "wine";
}
}
class SparklingWine extends Wine {
@Override
String name() {
return "sparkling wine";
}
}
class Champagne extends SparklingWine {
@Override
String name() {
return "champagne";
}
}
public class Overriding {
public static void main(String[] args) {
Wine[] wines = { new Wine(), new SparklingWine(), new Champagne() };
for (Wine wine : wines)
System.out.println(wine.name());
}
}
鉴于重载方法的这种特征,而语言本身对其也没有特别的限制,作者建议<不要提供相同参数数量的重载方法>,而对于可变参数则不要考虑重载。
关于这个建议的不错的例子就是ObjectOutputStream,对于其write方法,设计者并没有提供相同参数数量的重载,而是提供了诸如writeInt,writeBoolean,writeLong等方法,而且read方法也是与write对称的。
这种方式不适用于构造器,我们无法对构造器进行命名,但我们可以对构造器进行私有化并导出静态工厂。
但也并不能说必须严格遵守这种规则,比如为fetchSalaryInfo()
提供了两种方法重载,分别是int uid和User userInfo,很难想象会出现调用错误的情况。
破坏<不要提供相同参数数量的重载方法>这一规则不仅仅是出现在导出某个方法的时候,更新现有类的时候也有可能。
比如String类有一个since 1.4的contentEquals(StringBuffer),另外还有一个since 1.5的contentEquals(CharSequence)。
(Java 1.5版本中增加了CharSequence接口,并作为StringBuffer,StringBuilder,CharBuffer,String等类的公共接口)
但这并不会带来危害,因为这个例子的两个重载方法的行为是完全一样的。
对于泛型还有下面这种有趣的情况:
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class SetList {
public static void main(String[] args) {
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里的参数是index还是element?
执行结果是[-3,-2,-1][-2,0,2],也就是说 list.remove中的参数是index,而不是被自动装箱。
应该说问题根本原因是List同时提供了remove(int)
和remove(E)
吗?
但作为API的使用者,我们能做的仅仅是对Java 5的这一大特性持更谨慎的态度。
标签:
原文地址:http://www.cnblogs.com/Kavlez/p/4278007.html