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

来源:互联网 发布:java集合框架容器 编辑:程序博客网 时间:2024/06/05 20:15

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


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

对于构造器,则应该避免这样的情形:同一组参数只需经过类型转换,就可以被传递给不同的重载构造器。
否则,程序员会很迷茫,到底应该调用哪个构造器。

如果上面的情形都无法避免,则应当保证:当传递同样的参数时,所有重载方法的行为必须一致!如果不能做到这一点,程序员就很难有效的使用重载方法或构造器的,他们就不能理解它为什么不能正常工作。

0 0