Effective Java 读书笔记——41:慎用重载

来源:互联网 发布:node socket.io聊天室 编辑:程序博客网 时间:2024/06/17 17:04

先看一个使用重载错误的例子:

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));    }}

我们希望打出的是,set,list,Unknown Collection。实际上,它的输出是:

Unknown CollectionUnknown CollectionUnknown Collection
首先,classify方法在这里被重载了是肯定的,事实上要调用哪个方法进行重载,在编译时就已经做出决定了。循环中的三个类,编译器都认为是Collection<?>类,所以输出的是三个Unknown Collection。

在这里,把函数的重载和多态混淆了。方法的覆盖用来实现多态,这才是动态的,在运行时选择被覆盖的方法。

下面是一个多态的典型正确例子,在调用被覆盖方法的时候,编译的类型不会有影响,根据new的类型的方法将会被执行:

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());    }}



如果想修正第一个例子,可以在classify的内部使用instanceof做一个显示的测试,来得到结果:

public static String classify(Collection<?> c) {return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";}


如果对于API来说,普通用户根本不知道对于一组给定的参数,到底会重载哪个函数的时候,那么这样的API就很容易出错。而且这类错误只有等到程序出现非常怪异的行为的时候才能被发现,而且不容易诊断错误。因此,尽量避免胡乱使用重载。

使用重载的原则

  1. 永远不要导出两个具有相同参数数目的重载方法。尽量使重载方法的参数数量不同;对于使用的可变参数,最好不要重载。
  2. 如果一定要重载,那么对于一对重载方法,至少要有一个对应的参数在两个重载方法中的类型“完全不同”。这样一来,就不可以把一种实例转换为另一种实例,相对来说是保守安全的。
针对上面第2条,给出一个典型的错误例子:
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);}}

可以看到,在第一个循环中,依次加入了-3,-2,-1,0,1,2,在第二个循环中,试着删除0,1,2,希望留下-3,-2,-1。可是输出结果是:
[-3, -2, -1] [-2, 0, 2]

Set的输出结果如同我们想的一样,但是List的结果不一样。实际发生的情况是:set.remove(E),选择重载方法的参数实际上是Integer,这里进行了自动装箱,把int装箱成了Integer;对于List,有两个重载函数,这里直接重载了list.remove(i),并没有重载到list.remove(E),是从list的指定位置进行remove,所以先移除了第0个,也就是-3,list中所有元素前移;再移除第1个,也就是list中当前第2个,也就是-1;以此类推,最后得到-2,0,2。我们可以在源码中看到:
    /**     * 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 = elementData(index);        int numMoved = size - index - 1;        if (numMoved > 0)            System.arraycopy(elementData, index+1, elementData, index,                             numMoved);        elementData[--size] = null; // clear to 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;    }

因此,如果想修正上面的代码,我们可以写成:

for (int i = 0; i < 3; i++) {set.remove(i);list.remove(Integer.valueOf(i));}


总结

实际情况往往比上面分析的复杂的多,确定选择哪个重载方法的规则也极其困难。有时候,新增的API可能会违背上面的规则,但是,只要重载的方法执行相同的功能,返回相同的结果,这样也是可以接受的。确保这种行为的标准做法是,让更具体化的重载方法把调用转发给更一般的重载方法去做

总之,能够重载并不意味者应该重载,一般来说,对于多个具有相同参数数目的重载方法,还是尽量避免使用重载。另外一些情况下,重载方法尽量把调用转发给一般的重载方法去做,不同的重载方法尽量保证行为一致



0 0
原创粉丝点击