ITEM 15: MINIMIZE THE ACCESSIBILITY OF CLASSES AND MEMBERS
将设计良好的组件与设计不良的组件区别开来的一个最重要的因素是组件对其他组件隐藏其内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,将API与其实现清晰地分离开来。然后,组件只通过它们的api进行通信,而不注意彼此的内部工作。这个概念称为信息隐藏或封装,是软件设计的基本原则。
信息隐藏之所以重要,原因有很多,其中大部分原因是它将系统的组件解耦,从而允许独立地开发、测试、优化、使用、理解和修改它们。这加快了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快地理解组件,并且可以调试或替换组件,而不用担心损害其他组件。虽然信息隐藏本身不会带来良好的性能,但它支持有效的性能调优:一旦系统完成并分析确定哪些组件导致性能问题,就可以在不影响其他组件正确性的情况下优化这些组件。信息隐藏增加了软件重用,因为那些没有紧密耦合的组件通常在其他上下文中很有用,除了它们是为之开发的那些。最后,信息隐藏降低了构建大型系统的风险,因为即使系统没有成功,单个组件也可能被证明是成功的。
Java有许多工具可以帮助信息隐藏。访问控制机制指定类、接口和成员的可访问性。实体的可访问性由其声明的位置决定,如果有访问修饰符(private、protected、public)存在于声明中,则由它决定。正确使用这些修饰符对于信息隐藏非常重要。
经验法则很简单:使每个类或成员尽可能不可访问。换句话说,使用与您正在编写的软件的正常功能相一致的尽可能低的访问级别。
对于顶级(非嵌套)类和接口,只有两种可能的访问级别:包私有和公共。如果使用公共修饰符声明顶级类或接口,它将是公共的;否则,它将是包私有的。如果可以将顶级类或接口设置为包私有,那么它应该是私有的。通过使其包私有,您可以使其成为实现的一部分,而不是导出API的一部分,并且您可以在后续版本中修改、替换或删除它,而不必担心会损害现有客户端。如果您将其公开,您有义务永远支持它以保持兼容性。
如果包私有顶级类或接口只由一个类使用,则考虑将顶级类作为使用它的惟一类的私有静态嵌套类。这将其可访问性从包中的所有类降低到使用它的一个类。但是,减少不必要的公共类的可访问性要比包私有顶级类的可访问性重要得多:公共类是包API的一部分,而包私有顶级类已经是其实现的一部分。
对于成员(字段、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里列出,以便提高可访问性:
- private,成员只能从声明它的顶级类中访问。
- package-private,成员可以从声明它的包中的任何类访问。在技术上称为默认访问,如果没有指定访问修饰符(接口成员除外,接口成员在默认情况下是公共的),这就是您所获得的访问级别。
- protected,成员可以从声明它的类的子类访问,也可以从声明它的包中的任何类访问。
- public,该成员可以从任何地方访问。
在仔细设计了类的公共API之后,您应该让所有其他成员都是私有的。只有当同一包中的另一个类确实需要访问成员时,才应该删除私有修饰符,使其成为 package-private。如果您发现自己经常这样做,您应该重新检查系统的设计,看看另一个分解是否可能产生更好地彼此解耦的类。也就是说,私有成员和包私有成员都是类实现的一部分,通常不会影响其导出的API。但是,如果类实现 Serializable,这些字段可能会“泄漏”到导出的API中。
对于公共类的成员,当访问级别从 package-private 变为 protected 时,可访问性会有很大的提高。protected 成员是类导出API的一部分,必须永远支持。此外,导出类的 protected 成员表示对实现细节的公开承诺。对 protected 成员的需求应该相对较少。
有一个关键规则限制您降低方法可访问性的能力。如果一个方法覆盖了一个超类方法,那么它在子类中的访问级别不可能比在超类中有更多的限制。这对于确保子类的实例在超类的实例可用的任何地方都可用是必要的(Liskov替换原则,见第15项)。如果违反此规则,编译器将在您尝试编译子类时生成一条错误消息。这个规则的一个特殊情况是,如果一个类实现了一个接口,那么接口中的所有类方法都必须在类中声明为public。
为了方便测试您的代码,您可能会尝试使类、接口或成员比正常情况下更容易访问。这在某种程度上是好的。将公共类包的私有成员设置为私有以测试它是可以接受的,但是将可访问性提高到更高是不可接受的。换句话说,将类、接口或成员作为package-private 导出API的一部分以方便测试是不可接受的。幸运的是,也没有必要这样做,因为测试可以作为测试包的一部分运行,从而获得对包私有元素的访问权。
公共类的实例字段很少应该是公共的(item 16)。如果实例字段是非 final 的,或者是对可变对象的引用,那么通过将其公开,您就放弃了限制可以存储在该字段中的值的能力。这意味着您放弃了强制包含字段的不变量的能力。此外,您还放弃了在修改字段时采取任何操作的能力,因此具有公共可变字段的类通常不是线程安全的。即使字段是 final 并引用不可变对象,通过将其公开,您就放弃了切换到不存在字段的新的内部数据表示形式的灵活性。
同样的建议也适用于静态字段,但有一个例外。您可以通过公共静态 final 字段公开常量,假设常量构成类提供的抽象的一个完整部分。按照惯例,这些字段的名称由大写字母组成,单词由下划线分隔(item 68)。关键是这些字段要么包含原始值,要么包含对不可变对象的引用(item 17)。包含对可变对象引用的字段具有非 final 字段的所有缺点。虽然不能修改引用,但是可以修改被引用的对象——这会导致灾难性的结果。
注意,非零长度数组总是可变的,因此类拥有公共静态最终数组字段或返回此类字段的访问器是错误的。如果类有这样的字段或访问器,客户端将能够修改数组的内容。这是安全漏洞的一个常见来源:
// Potential security hole!
public static final Thing[] VALUES = { ... };
注意,有些 IDE 生成访问器,这些访问器返回对私有数组字段的引用,从而导致了这个问题。有两种方法可以解决这个问题。您可以使公共数组私有,并添加一个公共不可变列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)) ;
或者,您可以使数组私有,并添加一个公共方法,返回一个私有数组的副本:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
要在这些选项中进行选择,请考虑用户可能对结果做什么。哪种返回类型更方便?哪个会有更好的表现?
从Java 9开始,作为模块系统的一部分引入了另外两个隐式访问级别。模块是包的分组,就像包是类的分组一样。模块可以通过模块声明中的导出声明显式地导出它的一些包(按照惯例,模块声明包含在名为module-info.java的源文件中)。模块内未导出包的公共成员和受保护成员在模块外不可访问;在模块中,可访问性不受导出声明的影响。使用模块系统允许您在模块内的包之间共享类,而不需要使它们对整个世界可见。未导出包中的公共类的 public 和 protected 成员产生两个隐式访问级别,它们是正常公共和受保护级别的模块内类似物。这种共享的需求相对较少,通常可以通过重新安排包中的类来消除。
与四个主要访问级别不同,这两个基于模块的级别主要是建议。如果你把一个模块的JAR包放在应用程序的类路径,而不是模块路径,包将恢复到非模块化的行为: 包的公共成员和保护成员都具有正常的可访问性,不管包是否由模块导出。新引入的访问级别严格执行的一个例子是JDK本身:Java库中未导出的包在模块之外是无法访问的。
模块对典型的Java程序员提供的访问保护功能有限,而且在本质上主要是建议性的;为了利用它,您必须将包分组到模块中,在模块声明中显式地显示它们的所有依赖关系,重新排列源代码树,并采取特殊的操作来适应从模块中对非模块化包的任何访问[Reinhold, 3]。现在就断言模块是否会在JDK本身之外得到广泛使用还为时过早。与此同时,除非你有迫切的需要,否则最好避免使用它们。
总之,您应该尽可能减小程序元素的可访问性(在合理范围内)。在仔细设计了最小的公共API之后,您应该防止任何游离的类、接口或成员成为API的一部分。除了用作常量的公共静态 final 字段之外,公共类应该没有公共字段。确保公共静态 final 字段引用的对象是不可变的。