区分良好设计与不良设计的组件的最重要的因素是,这个组件对其他组件隐藏它的内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地把它的API从它的实现中分离。然后组件仅仅可以通过它们的API通讯,而且不知道它们彼此的内部运作。这个概念,叫信息隐藏(information hiding)或者封装(encapsulation),是软件设计的一个基础原则[Parnas72]。
信息隐藏在许多方面是重要的,大多数起源于这个事实:它解耦(decouples)了组成系统的组件,让它们可以开发、测试、优化、使用、理解和隔离修改。这加快了系统开发,因为组件可以并行地开发。它减轻了维护的负担,因为组件可以更快地理解、调试或者不要担心危害其他组件地替代。虽然信息隐藏和它本身并不能带来好的性能,但是它使得性能调优有效率:一旦一个系统完成了,性能分析决定了哪个组件造成了性能问题(条目67),这些组件可以没有影响其他组件正确的情形下优化。信息隐藏增加了软件复用,因为没有紧耦合的组件,除了它们被开发的情形下,常常证明在其他情形下是有用的。最后,信息隐藏减少了构建大系统的危险性,因为单个组件可以证明是成功的,即使系统并没有。
Java有协助信息隐藏的许多工具。访问控制(access control)机制[JLS, 6.6] 规定了类、接口和成员的访问性(accessibility)。实体的访问性由下面两者决定:它的声明位置和在声明时呈现哪个(如果有)访问标识符(private、protected和public)。这些修饰符正常使用对于信息隐藏是必要的。
经验法则是简单的:使得每个类或者成员尽量不可访问。换句话说,使用与你编写软件的正常功能一致的尽可能低的访问等级。
对于顶层的(非嵌套的)类和接口,有两个可能的访问等级:包私有的(package-private)和公开的(public)。如果你声明了一个公开修饰符的顶层类或者接口,它是公开的;否则,它是包私有的。如果顶层的类或者接口是包私有的,它应该是这样。通过使得它是包私有的,你可以使得它是实现的一部分,而不是可以导出的API,而且你可以修改它、替代它,或者在下一部的发布中移除它,而不用担心已经存在的客户端。如果你使得它是公开的,那么你有义务为了维护兼容性而永久支持它。
如果是包私有的顶层类或者接口仅仅由一个类使用,考虑把顶层类变成是使用它的唯一类的私有静态嵌套类(条目24)。这减少了它的包里面的所有类对使用它的那个类的访问。但是相对于包私有的顶层类,减少一个不必要的公开类的访问重要得多:公开类是包的API,而包私有的顶层类已经是它实现的一部分。
对于成员(域、方法、嵌套类和嵌套接口),有四种可能的访问等级,以增加访问性顺序列出如下:
私有的(private) -- 成员仅仅可以从在声明它的顶层类里访问
包私有的(package-private) -- 成员可以从在声明它的包里面的任何类访问。技术上被认为是默认的访问。如果没有指定访问修饰符(除了接口成员,它默认是公开的),这是你得到的访问等级。
受保护的(protected) -- 成员可以从声明它的类的子类访问(受到一些限制[JLS, 6.6.2]),而且可以从声明它的包里面的任何类访问。
公开的(public) -- 成员可以从任何地方访问。
在小心地设计你的类的公开API之后,你第一反应应该是使得所有其他的成员是私有的。只有在同一个包里面的另外一个类真正需要访问一个成员,你才应该移除私有修饰符而使得这个成员是包私有的。如果你发现自己经常这么做,你应该重新检测系统的设计,看看从另一个类解耦比较好的类是否需要另外一个分解。也就是说,私有的和包私有的成员是类实现的一部分,而通常不会影响它的导出API。然而,如果类实现了系列化(Serializable)(条目86和87),这些域可能泄漏到导出的API。
对于公开类的成员,当访问等级从包私有到受保护时,访问性的急剧增加将会发生。访问的对象是类导出API的一部分,而且必须永远支持。而且,一个导出类的受保护的成员代表着实现细节的一个公开承诺(条目19)。受保护成员的需求是相等少见的。
有个重要的规则是,限制你的能力来减少方法的访问性。如果一个方法覆写了一个超类,在子类中它不能有比超类中更严格的访问等级[JLS, 8.4.8.3]。这是必要的,保证子类实例在超类实例可用的地方都可以使用(里氏代换原则(Liskov substitution principle),参考条目15)。如果你违反了这个规则,那么当你试着编译子类时编译器将会产生一个错误信息。这个规则的特例是,如果类实现了一个接口,所有接口中的类方法在类中必须声明为公开的。
为了方便测试你的代码,你可能倾向于让一个类、接口或者成员有比原本需要的更多访问性。这在某种程度是可以的。为了测试让一个公开类的私有成员成为包私有的,这是可接受的,但是再提高可访问性是不可接受的。换句话说,为了方便测试,让一个类、接口或者成员成为一个包导出API一部分,这是不可接受的。幸运的是,我们也没必要,因为测试可以作为需要测试包的一部分而运行,所以可以访问它的包私有元素。
公开类的实例方法应该很少是公开的(条目16)。如果一个实例域是非final的或者是一个可变对象的引用,让它成为公开的,那么你放弃了限制存储在域中的值的能力。这意味着,这你放弃了实现这个域成为不变类的能力。而且,当类改变的时候,你放弃了采取任何行动的能力,所以有公开可变域的类通常不是线程安全的。即使一个域是final而且引用了一个可变对象,让它成为公开的,你就放弃了切换到一个新内部数据呈现(这个域不存在)的灵活性。
同样的建议可以应用到静态域,但是有一个例外。你可以通过公开静态final域来暴露常量,假设这些常量是组成这个类提供的抽象的不可分割的一部分。按照惯例,这些域有大写字符的名字,单词用下划线分割(条目68)。这些域要么包含原始值,要么包含对不可变对象的引用(条目17)。一个包含可变对象引用的域,有非final域的所有缺点。虽然这个引用不可能改变,但是被引用的对象可以修改--有灾难性的结果。
注意到,非零长度的队列总是可变的,所以一个类有一个公开静态final队列域或者有返回这种域的访问子(accessor)是错误的**。如果一个类有这样的域或者访问子,那么客户端可以修改队列的内容。下面是一个安全漏洞的常见来源:
// 潜在的安全漏洞!
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();
}
为了从这些备选方案选择,想想客户端可能对结果做什么。哪个返回类型更方便?哪个将有更好的性能?
在Java9中,有两个额外的隐含访问等级引入为模块系统(module system)的一部分。一个模块是一组包,就像包是一组类。一个模块通过在它的模块声明(module declaration)(通常包含在一个叫做module-info.java的源文件)中导出声明(export declaration),可以显式地导出它的有些包。模块里未导出包的公开和受保护成员在模块外面是不可访问的;在模块里面,访问性是不受导出声明影响的。使用模块系统让你可以在模块里面的包之间分享类,而没有让它们对于整个世界可见。在未导出包里面的公开类的公开和受保护成员,导致了两个隐含的访问等级,它们可类比于正常公开和受保护等级。这种分享的需求是相当少见的,而且常常可能通过重新安排包里面的类来解决。
不像四个主要的访问等级,这两个基于模块的等级是建议性质的。如果你在你的应用的类途径而不是它的模块途径上放置一个模块的JAR文件,那么这个模块的包恢复到它们的非模块化行为:包的公开类的所有公开和受保护成员有它们的正常访问性,而不管包是否有这个模块导出[Reinhold, 1.2]。新引入的访问等级被严格执行的地方,是JDK本身:Java库中的非导出包在它们的模块外是真正不可访问的。
对于一个典型的Java程序员,不只是由模块提供的访问保护是功能有限的,而且在本质上是建议性质的;为了利用它,你必须把你的包分组到模块中,使得它们的所有依赖在模块声明是明显的,重新安排你的源码树,而且采取特别的行动:在你的模块里面容纳对非模块化包的任何引用[Reinhold, 3]。现在就说模块在JDK本身之外是否能广泛使用还为时过早。同时,除非你有一个迫切的需求,似乎最好是避免它们。
总之,你应该尽可能减少(明智地)对程序元素的访问性。在仔细设计一个最小限度公开API后,你应该阻止散落的任何类、接口或者成员成为API的一部分。除了作为常量的公开静态final域,公开类不应该有公开域。确保由公开静态final域引用的类是不可变的。