您的位置:首页 > Web前端

Effective Java读书笔记十五(Java Tips.Day.15)

2017-05-19 00:47 190 查看

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语言的一部分后,谨慎重载显得更加重要了。

总之,一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载。可以使用不同的方法名来达到目的。

对于构造器,则应该避免这样的情形:同一组参数只需经过类型转换,就可以被传递给不同的重载构造器。

否则,程序员会很迷茫,到底应该调用哪个构造器。

如果上面的情形都无法避免,则应当保证:当传递同样的参数时,所有重载方法的行为必须一致!如果不能做到这一点,程序员就很难有效的使用重载方法或构造器的,他们就不能理解它为什么不能正常工作。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 读书笔记 api 设计