第四十一条 慎用重载


本章节一开始就给了一个例子,泛型+数组类型的,

public class CollectionClassifier {
    
    public static String classify(Collection<?> c) {
        return "UnknownCollection";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Set<?> s) {
        return "Set";
    }

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

我们可以看出,classify()方法有三个,这叫做重载。这个例子很明显, collections 数组里有三个不同类型的集合,我们想通过不同集合类型,打印相对应的值,通过for循环,一次打印 set list和 map 对应的字段,但很可以,打印出来的值都是"UnknownCollection", 这说明 for 循环中的三次方法调用,都调用了 classify(Collection<?> c) 方法,这是为什么呢?重载时,调用哪个函数,是在编译时期决定的,之前提到过,泛型是编译时擦除痕迹,尤其是和数组搭配时,很容易出问题,在这个地方,由于泛型的关系,所以数组里面能识别的都按照 Collection 类型了,这也是为什么打印了 三个 "UnknownCollection" 值。对于这种重载,要慎重。如果要使用,最好重载时,确保参数的个数不一致,不要用相同的参数个数,仅仅是改变了参数的类型,上面的例子算是个比较典型的反例。

说到了重载,还有一个叫做重写。就是一个父类有个方法,他的子类也有个相同的方法,参数和方法名都一样,子类重新实现了这个功能,这就叫做重写。书中也举了个例子,

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

这个程序打印出来的结果是 "wine"  "sparkling wine"  "champagne",符合我们的预期。这是为什么呢,因为这些方法都是在class类内部,编译时都是 Wine 对象,但是这里涉及到了多态,还是会找到自己的具体的类型,所以找到的都是真实类型的对象,而非是父类,编译的类型不会有影响,因此打印出了正确的log。

重载和重写的概念不太一样,对于上面重载的反例,如果我们想加以识别,可以用 instanceof 来识别具体的类型,来做判断

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

这样,根据 instanceof 关键字,就能识别了。重载和重写一般是被一块提起的,对于重载,我们一般慎用,或者通过不同参数个数来控制,记住上面重载的反例,下面还有个例子

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实际是 TreeSet, 看它的remove()方法, TreeSet 实际上是有个 TreeMap 来替它保存数据,

    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
这里的 m 其实就是一个 TreeMap,因为上面的new TreeSet<Integer>() 调用的是无参构造,里面使用的是默认的 TreeMap,此时,调用的是 TreeMap 的remove()方法

    public V remove(Object key) {
        TreeMapEntry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }

    final TreeMapEntry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        TreeMapEntry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

看到这里就明白了,set 删除时,把 int 值穿进去,然后进行了自动装箱,把int变为了Integer,然后重载了 remove()方法,此时,Set 里面装的对象也是装箱类型的,所以就正确的执行了我们希望的逻辑,再看看List 为什么出错,在这里,List 对应的是 ArrayList ,看看它的源码,

    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(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; // clear to let GC do its work

        return oldValue;
    }

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

看到这,发现里面有两个 remove()方法,我们知道,int类型放入集合时,实际上是自动装箱的,也就是说ArrayList和TreeSet中都是以Integer的格式存在的,我们再看看这两个重载的方法,remove(int index) 是根据索引删除的,此时,传进去的 int 值是该数组的索引;remove(Object o) 这个方法才是删除对象的,此时穿进去如果是 Integer的话,会调用这个方法,此时根据对象的
值来找到相应的索引,然后根据索引删除数据。上面list删除时,调用的是 remove(int index) 方法,传进去的int值没有自动装箱,所以原始数据是 [-3,-2,-1,0,1,2], 删除 remove(0),把索引为0的删除了,也就是删除了-3,所以数组变为 [-2,-1,0,1,2], 然后 remove(1),把索引为1的元素删除了,也就是 -1, 所以变为了 [-2,0,1,2], 接着remove(2),把索引为2的删除了,也就是删
除了1,所以剩余数组是 [-2, 0, 2],与上述一致。 所以,这个具体是怎么删除,还是要看源代码的,通俗来说,是重载使用的不对,容易造成误判。如果想要list也删除对象,可以对int进行装箱,如下

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

remove()时,把int包装为 Integer ,这样,就执行 remove(Object o) 方法,就可以了。为了以防万一,慎用重载;使用重载,切记要参数个数不同,不要写出方法个数相同的重载。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值