第51条说,应该使用接口而不是类作为参数类型。更一般地说,您应该更喜欢使用接口而不是类来引用对象。如果存在适当的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。真正需要引用对象的类的惟一时间是使用构造函数创建它的时候。为了具体说明这一点,考虑LinkedHashSet的情况,它是Set接口的一个实现。养成这样的习惯:
而不是:
如果您养成了使用接口作为类型的习惯,那么您的程序将更加灵活。如果您决定要切换实现,只需更改构造函数中的类名(或者使用不同的静态工厂)。例如,第一个声明可以改为:
所有的代码都会继续工作。周围的代码不知道旧的实现类型,所以它不会注意到更改。
有一个警告:如果原始实现提供了接口的通用契约不需要的一些特殊功能,并且代码依赖于该功能,那么新实现提供相同的功能是至关重要的。例如,如果围绕第一个声明的代码依赖于LinkedHashSet的排序策略,那么在声明中将HashSet替换为LinkedHashSet将是不正确的,因为HashSet不保证迭代顺序。
那么,为什么要更改实现类型呢?因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。例如,假设一个字段包含一个HashMap实例。将其更改为EnumMap将提供更好的性能和与键的自然顺序一致的迭代顺序,但是您只能在键类型为enum类型的情况下使用EnumMap。将HashMap更改为LinkedHashMap将提供可预测的迭代顺序,性能与HashMap相当,而不需要对键类型作出任何特殊要求。
您可能认为使用变量的实现类型声明变量是可以的,因为您可以同时更改声明类型和实现类型,但是不能保证这种更改会导致编译程序。如果客户机代码对原始实现类型使用了替换时不存在的方法,或者客户机代码将实例传递给需要原始实现类型的方法,那么在进行此更改之后,代码将不再编译。使用接口类型声明变量可以保持诚实。
如果不存在合适的接口,则完全可以使用类而不是接口来引用对象。例如,考虑值类,如String和BigInteger。值类很少在编写时考虑到多个实现。它们通常是final的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。
没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口。如果一个对象属于这样一个基于类的框架,那么最好使用相关的基类来引用它,这通常是抽象的,而不是使用它的实现类。许多java.io类比如OutputStream属于这类。
没有合适接口类型的最后一种情况是实现接口但同时提供接口中没有的额外方法的类——例如,PriorityQueue有一个在队列接口上不存在的比较器方法。只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实践中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和时尚。如果没有合适的接口,就使用类层次结构中提供所需功能的最不特定的类。
本文写于2019.7.19,历时1天