TIP 40 谨慎设计方法签名
本条目会帮助你设计更好的API
请遵循标准的命名习惯
- 名称应易于理解,而且与同一个包中的其它名称风格一致
- 选择与大众认可的名称相一致,或者自然语言中相同含义的词汇作为名称
- 不要过于追求提供便利的方法
不要过于追求提供便利的方法
- 对于类和接口所支持的每个动作,都提供一个功能齐全的方法。
- 每个方法应该尽其所能,不要设计太多太散的方法,否则难以学习、使用、文档化、测试和维护。
- 当一项操作被经常用到的时候,才考虑为它提供快捷方式——提炼为一个方法。
避免过长的参数列表
不要设计超过4个参数的方法,否则
- 难以使用参数太多的方法,很可能还需要不停的参考文档。
- 如果长参数序列的类型相同,简直就是噩梦。如果不小心弄错顺序,方法可以正常执行,但不会按照程序员的意图正常工作。
如果你遇到了这种需求,请考虑使用以下方法来避免:
- 把方法分解成多个方法,每个方法只需要这些参数的一个子集。如果一个类的构造方法的参数太多,可以考虑减少构造方法的参数数量,其它的参数序列用set方法来设置。
- 创建辅助类,用来保存参数的分组。这些辅助类一般为静态成员类(TIP 22)。例如你正在编写一个表示纸牌游戏的类,你会发现经常要传递一双参数来表示花色和点数。这时就可以考虑设计一个静态成员类——纸牌类,这个纸牌类拥有花色和点数这两个field,然后纸牌游戏类的API以及它的内部表示都可以使用这个纸牌类。
- 结合以上两点,从对象构建和方法调用都采用Builder模式,参考TIP 2。
优先使用接口而不是类来表示参数类型
只要有适当的接口可以用来定义参数,就优先使用接口,而不是实现这个接口的类。这也是我们多次说过的,面向接口编程的设计理念。
- 比如,如果一个方法的参数类型是HashMap,就考虑用Map接口作为参数类型,这样你可以传入HashTable、HashMap、TreeMap、TreeMap的子映射表,或者任何其它实现了Map的类型作为方法的参数。如果使用的参数是类而不是接口,则限制了客户端智能传入特定的实现类型。
- 对于boolean参数,优先使用两个元素的枚举类型。例如,设计一个Thermometer(温度计)类型,它带有一个静态方法:
public static Thermometer create(boolean isFahrenheit)
,参数为true则使用华氏度单位,false则使用摄氏度单位。然而,使用枚举来代替boolean,会使代码更易于阅读和编写:
public enum TemperatureScale{ FAHRENHEIT,CELSIUS;}
public static Thermometer create(TemperatureScale temperatureScale)
TIP 41 慎用重载
先来看看这段代码,判断一下运行结果会是怎样:
public class CollectionClassifier {
public static String classify(Set<?> set){
return "Set";
}
public static String classify(List<?> set){
return "List";
}
public static String classify(Collection<?> set){
return "Unkown Collection";
}
public static void main(String args[]){
Collection<?>[] collections = {
new HashSet<String>(),
new LinkedList<BigInteger>(),
new HashMap<String ,String>().values()
};
for (Collection<?> c :collections) {
System.out.println(classify(c));
}
}
}
显然,这个classify方法有三个重载版本,而作者期望的运行结果是:
期望运行结果:
Set
List
UnKnown Collection
但是很遗憾,三个重载的版本中,只有参数类型为Collection<?>
的版本被调用了:
实际运行结果:
UnKnown Collection
UnKnown Collection
UnKnown Collection
重载方法与覆盖方法不同。
覆盖方法——上转型——多态,这个机制的本质是方法的运行时动态绑定。被覆盖的实例方法,总会选择具体的类型作为参数。
但重载与覆盖不同,不会发生方法的动态绑定行为。实际上具体要调用哪个重载版本,编译期就会被决定,而不会等到运行时。
实际上豆爷在用IDEA敲出以上代码后,在运行之前,编译器已经发出了提醒 :
classify(Set<?> set)
和classify(List<?> set)
is never used.
那么,怎样才能保证合适的、正确的重载呢?
安全而保守的策略是,永远不要编写出两个具有相同参数数目的同名方法。而如果方法的参数列表是一个可变参数,那么永远不要重载它。
如果遵守这些规则,程序员就不会陷入到“我到底应该用哪个重载方法”的懵逼状态。
再来看个重载相关的例子:
public class SetList {
public static void main(String args[]) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++){
set.add(i);
list.add(i);
}
System.out.println(set + " " + list);
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
显然,该类的作者期望这样的运行结果:
[-3, -2, -1, 0, 1, 2] [-3, -2, -1, 0, 1, 2]
[-3, -2, -1] [-3, -2, -1]
也就是说,本来期望程序删除set和list中的0,1,2数字
但事与愿违:
[-3, -2, -1, 0, 1, 2] [-3, -2, -1, 0, 1, 2]
[-3, -2, -1] [-2, 0, 2]
set算是正常的,但list没有得到期望的结果。
问题就在于, set.remove(i)调用选择重载方法remove(E), 这里的参数E是 集合<Integer>
的元素类型, 将i从int自动装箱到Integer中,这正好是期待的行为。因此set部分能正常工作。
然而,list.remove(i)调用选择重载方法remove(int i),它从索引位置i去除元素。因此, list的调用过程是这样的:
list.remove(0); //删除了值:-3 ,此时list: [-2, -1, 0, 1, 2]
list.remove(1); //删除了值:-1 ,此时list: [-2, 0, 1, 2]
list.remove(2); //删除了值:1 ,此时list: [-2, 0, 2]
显然,List重载了 remove(E e)
和 remove(int i)
方法,当它在Java 1.5版本中被泛型化之前,List接口有一个 remove(Object o)
而不是 remove(E e)
, 而相应的参数Object 和 int是根本不同的类型,因此程序员永远不会搞混这两个重载版本。
但自从有了泛型和自动装箱机制后,这两种参数类型就不再根本不同了。换句话说,泛型和自动装箱机制破坏了List接口。幸运的是,Java类库中几乎再没有API受到同样的破坏。
这个例子充分的说明了,自动装箱和泛型成为Java语言的一部分后,谨慎重载显得更加重要了。
总之,一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载。可以使用不同的方法名来达到目的。
对于构造器,则应该避免这样的情形:同一组参数只需经过类型转换,就可以被传递给不同的重载构造器。
否则,程序员会很迷茫,到底应该调用哪个构造器。
如果上面的情形都无法避免,则应当保证:当传递同样的参数时,所有重载方法的行为必须一致!如果不能做到这一点,程序员就很难有效的使用重载方法或构造器的,他们就不能理解它为什么不能正常工作。