Effective Java 读书笔记——41:慎用重载
2017-02-02 11:27
225 查看
先看一个使用重载错误的例子:
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 Collection
Unknown Collection
Unknown 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就很容易出错。而且这类错误只有等到程序出现非常怪异的行为的时候才能被发现,而且不容易诊断错误。因此,尽量避免胡乱使用重载。
如果一定要重载,那么对于一对重载方法,至少要有一个对应的参数在两个重载方法中的类型“完全不同”。这样一来,就不可以把一种实例转换为另一种实例,相对来说是保守安全的。
针对上面第2条,给出一个典型的错误例子:
可以看到,在第一个循环中,依次加入了-3,-2,-1,0,1,2,在第二个循环中,试着删除0,1,2,希望留下-3,-2,-1。可是输出结果是:
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。我们可以在源码中看到:
因此,如果想修正上面的代码,我们可以写成:
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(Integer.valueOf(i));
}
实际情况往往比上面分析的复杂的多,确定选择哪个重载方法的规则也极其困难。有时候,新增的API可能会违背上面的规则,但是,只要重载的方法执行相同的功能,返回相同的结果,这样也是可以接受的。确保这种行为的标准做法是,让更具体化的重载方法把调用转发给更一般的重载方法去做。
总之,能够重载并不意味者应该重载,一般来说,对于多个具有相同参数数目的重载方法,还是尽量避免使用重载。另外一些情况下,重载方法尽量把调用转发给一般的重载方法去做,不同的重载方法尽量保证行为一致。
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 Collection
Unknown Collection
Unknown 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就很容易出错。而且这类错误只有等到程序出现非常怪异的行为的时候才能被发现,而且不容易诊断错误。因此,尽量避免胡乱使用重载。
使用重载的原则
永远不要导出两个具有相同参数数目的重载方法。尽量使重载方法的参数数量不同;对于使用的可变参数,最好不要重载。如果一定要重载,那么对于一对重载方法,至少要有一个对应的参数在两个重载方法中的类型“完全不同”。这样一来,就不可以把一种实例转换为另一种实例,相对来说是保守安全的。
针对上面第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可能会违背上面的规则,但是,只要重载的方法执行相同的功能,返回相同的结果,这样也是可以接受的。确保这种行为的标准做法是,让更具体化的重载方法把调用转发给更一般的重载方法去做。总之,能够重载并不意味者应该重载,一般来说,对于多个具有相同参数数目的重载方法,还是尽量避免使用重载。另外一些情况下,重载方法尽量把调用转发给一般的重载方法去做,不同的重载方法尽量保证行为一致。
相关文章推荐
- (41):慎用重载
- Effective Java 读书笔记——42:慎用可变参数
- Effective Java 读书笔记——71:慎用延迟初始化
- Effective Java 英文 第二版 读书笔记 Item 6:Eliminate obsolete object references
- Effective Java 中文版_1_个人读书笔记.doc
- [Effective Java 读书笔记] 第三章类和接口 第十八--十九条
- effective java 读书笔记---第四章类与接口
- effective java 读书笔记---第六章 枚举与注解
- 《Effective java 第2版》读书笔记--创建/销毁对象
- 【读书笔记】《Effective Java》(5)--枚举和注解
- 《C++ Primer》读书笔记 第14章:重载运算与类型转换
- 慎用重载(effective java)
- 【读书笔记】《Effective Java》——目录
- 《Effective Java》读书笔记06--避免使用终结方法
- Effective Java 读书笔记
- [读书笔记]C++基础知识温习:重载递增/减运算符
- C++ 读书笔记之 重载 Overloading
- Effective Java 读书笔记(一):使用静态工厂方法代替构造器
- Effective Java 读书笔记(二)
- 《Effective Java》读书笔记