第51条建议:应该使用接口而不是类作为参数类型。更通俗来讲,应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类。为了更具体地说明这一点,以LinkedHashSet的情形为例,它是Set接口的一个实现。在声明变量的时候应该养成这样的习惯:
// Good - uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();
而不是像这样的声明:
// Bad - uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
如果养成了用接口作为类型的习惯,程序将会更加灵活
。当你决定更换实现时,所要做的只是改变构造器中类的名称(或者使用一个不同的静态工厂)。例如,第一个声明可以被改变为:
Set<Son> sonSet = new HashSet<>();
周围的所有代码都可以继续工作。周围的代码并不知道原来的实现类型,所以它们对于这种变化并不在意。
有一点值得注意:如果原来的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点是,新的实现也要提供同样的功能。例如,如果第一个声明周围的代码依赖于LinkedHashSet的同步策略,那么在声明中用HashSet代替LinkedHashSet就是不正确的,因为HashSet不能保证相关的迭代顺序。
为什么要改变实现类型呢?因为第二个实现提供了比第一个更好的性能,或者因为它提供了你所期望的而原来的实现缺乏的功能。比如,假设有一个域中包含了一个HashMap实例。如果将它改成EnumMap,则可以提供更好的性能,并且迭代顺序与键的自然顺序一致,但是如果键的类型为枚举类型,你就只能使用EnumMap。如果HashMap改成LinkedHashMap,则能提供可以预估的迭代顺序,以及可以与HashMap比拟的性能,对于键类型没有任何特殊的要求。
你可能会觉得,用变量的实现类型来声明变量,也是也可以接受,因为可以同时改变声明类型和实现类型,但是不能确保修改后的程序可以编译。如果客户端代码使用了没有出现在新实现中的原始类型中的方法,或者客户端代码将该实例传到了需要原始实现类型的方法中,那么代码在完成这样的修改之后将不再进行编译。使用接口类型声明变量可以保持诚实。
如果没有合适的接口存在,完全可以用类而不是接口来引用对象
。以值类为例,比如String和BigInteger。记住,值类很少会用多个实现编写。它们经常是final的,并且很少有对应的接口。使用这种值类作为参数、变量、域或者返回类型是再合适不过的了。
不存在适当接口类型的第二种情形是,对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应该用相关的基类来引用这个对象,而不是用它的实现类。许多java.io类,比如OutputStream就属于这种情形。
不存在适当接口类型的最后一种情形是,类实现了接口但它也提供了接口中不存在的额外方法,例如PriorityQueue有一个没有出现在Queue接口中的comparator方法。如果程序依赖于这些额外的方法,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
以上这些例子并不全面,而只是代表了一些适合于用类引用对象的情形。实际上,给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活。如果没有适合的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象吧
。