Walt You - 行是知之始

《Effective Java》学习日志(八)64:通过接口引用对象

2018-12-25


学习资料主要参考: 《Effective Java Third Edition》,作者:Joshua Bloch



第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。 值类很少用多个实现来编写。 它们通常是最终的,很少有相应的接口。 将这样的值类用作参数,变量,字段或返回类型是完全合适的。

第二种情况是没有适当的接口类型的是属于框架的对象,其基本类型是类而不是接口。 如果一个对象属于这样一个基于类的框架,最好用相关的基类来引用它,它通常是抽象的,而不是它的实现类。 许多java.io类(如OutputStream)都属于此类。

没有适当接口类型的最后一种情况是实现接口的类,但也提供了在接口中找不到的额外方法 - 例如,PriorityQueue具有Queue接口上不存在的比较器方法。 只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这应该是非常罕见的。

这三种情况并不是详尽无遗的,而仅仅是为了传达适合通过其类别引用对象的情况的风格。 在实践中,应该明确给定对象是否具有适当的接口。 如果是这样,如果您使用界面来引用对象,您的程序将更加灵活和时尚。 如果没有适当的接口,只需使用提供所需功能的类层次结构中最不具体的类。


Content