首页 > 学院 > 开发设计 > 正文

Effective Java

2019-11-14 22:54:49
字体:
来源:转载
供稿:网友
Effective java - 方法的参数声明

给方法的参数加上限制是很常见的,比如参数代表索引时不能为负数、对于某个关键对象引用不能为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.Collectionssort()方法,列表中的元素当然是都可比较的,而方法并没有在开头检查有效性,而是计算中遇到问题时抛出异常。与这种方式相比,提前检查有效性的意义确实不大。但在计算途中检查参数的有效性需要考虑一点,即方法的原子性。

说道参数就不得不说方法重载,首先上一段例子:

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的这一大特性持更谨慎的态度。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表