第41条: 慎用重载
下面是一个关于集合的例子,对set和list进行分类:
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。但是实际上,它的输出是3次Unknown Collection。为啥会这样,因为classify方法被重载了,而要调用哪个方法进行重载,在编译时就已经做出决定了。编译器都认为是Collection<?>类,所以输出的是三个Unknown Collection。
这个程序的行为有悖常理,因为对于重载方法(overloaded method ]的选择是静态的,而对于被覆盖的方法(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());
}
}
}
name方法是在类Wine中被声明的,但是在类SparklingWine和Champagne中被覆盖。正如你所预期的那样,这个程序打印出“wine, sparkling wine和champagne" ,尽管在循环的每次迭代中,实例的编译时类型都为Wine。当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法将被执行; “最为具体的(most specific)”那个覆盖版本总是会得到执行。
对classify方法的最佳修正方案是,用单个方法来替换这三个重载的classify方法,并在这个方法中做一个显式的instanceof测试:
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}
如果对于API来说,普通用户根本不知道"对于一组给定的参数,其中那个重载方法将会被调用",那么这样的API就很容易出错。而且这类错误只有等到程序出现非常怪异的行为的时候才能被发现,而且不容易诊断错误。因此,尽量避免胡乱地使用重载机制。
安全而保守的策略是:永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,根本不要重载它,除第42条中所描述的情况之外。
这里要特别注意的是类的构造器,因为你不可能吧构造器重新命名!可以考虑导出静态工厂。对于导出多个具有相同参数数目的重载方法时至少有一个对应参数在两个重载方法中具有"根本不同的类型"。
简而言之,能够重载 并不意味着就 应该重载。一般来说,对于多个具有相同参数数目的方法来说,应该避免重载方法。即使避免不了重载,也要保证避免同一组参数只需经过类型转换就可以被传递给不同的重载方法。