要区分精心设计的组件和设计不好的组件的最重要的因素在于,这个组件对于其他组件而言,是否隐藏了其内部数据和其他市县细节。设计良好的组件会隐藏其所有实现细节,把它的API与它的实现完全分离。组件之间只能通过它们的API进行通信,并且不知道彼此之间内部的工作细节。这种被称为信息隐藏(information hiding )或封装(encapsulation)的概念是软件设计的基本原则[Parnas72]。
信息隐藏之所以非常重要是有许多原因的,其中大部分原因在于它将构成系统的组件解耦,允许它们单独开发、测试、优化、使用、理解和修改。这样可以加快系统开发的速度,因为这些组件可以并行开发。它也减轻了维护的负担,因为程序猿可以更快地理解、调试或者更换组件,而不用担心会影响其他组件。虽然信息隐藏本身无论是对内还是对外,都不会带来更好的性能,但是它可以实现有效的性能调优:一旦系统完成,并通过分析确定了哪些组件影响了系统的性能(第67项),那些组件可以在不影响正确性的情况下对其他模块进行优化。信息隐藏提高了软件的可重用性,因为组件之间的耦合度低,除了开发这些组件所使用的环境之外,他们在其他的环境中往往也很有用。最后,信息隐藏也降低了构建大型系统的风险,因为即使整个系统不可用,这些独立的组件有可能是可用的。
Java有许多机制(facilities)来协助信息隐藏。访问控制机制(access control)机制[JLS, 6.6]制定了类、接口和成员的可访问性。实体的可访问性由其声明的位置确定,以及声明中出现的访问修饰符(private、protected和public)共同决定的。正确地使用这些修饰符对于信息隐藏至关重要。
经验法则很简单:尽可能使每个类或者成员不被外界访问。 换句话说,使用与你正在编写的软件的正常功能相一致的、尽可能低的访问级别。
对于顶层(非嵌套的)的类和接口,只有两种可能的访问级别:包私有(package-private)和公有的(public)。如果你用public修饰符声明了顶层类或者接口,那它就是公有的;否则,它将是包私有的。如果一个顶级类或者接口能够设置成包私有,它【的访问级别】就应该是包私有的。通过把类或者接口设置成包私有,你把它做成了实现的一部分而不是导出的API,在之后的发行版本中,可以对它进行修改、替换,或者删除,而无需担心会影响到现有的客户端。如果你把它做成公有的,你就有义务永远支持它,以保持它们的兼容性。
如果一个包级私有的顶层类或者接口只被一个类使用,请考虑将它成为唯一使用它的那个类的私有嵌套类(第24项)。这样就可以将它的可访问范围从包中的所有类缩小到了使用它的那个类。但是,降低不必要共有类的可访问性,比降低包级私有的顶层的更重要得多:因为共有类是包的API的一部分,而包级私有的顶层类则已经是这个包的实现的一部分。
对于成员(字段、方法、前套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:
-
私有的(private) ———— 只有在声明该成员的顶层类内部才可以访问这个成员。
-
包级私有的(package-private) ———— 声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)”访问级别,如果你没有声明访问修饰符,就是这个访问级别(除了接口的成员,接口成员默认是公有的)。
-
受保护的(protedted) ———— 声明该成员的类的子类可以访问这个成员(但有一些限制[JLS, 6.6.2]),并且,声明该成员的包内部的任何类也可以访问这个成员。
-
公有的(public) ———— 在任何地方都可以访问该成员。
在仔细地设计了类的公有API之后,你应该习惯性地把所有其他成员设置成私有的。只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,将该成员变成包级私有的。如果你发现自己经常这么做,你应该重新检查系统的设计,看看另一种分解方案是否能得到与其他类之间耦合度更小的类。也就是说,私有成员和包级私有成员都是一个类的实现中的一部分,一般不会影响它导出的API。但是,如果这个类实现了Serializable接口(第86、87项),这些字段就有可能“泄漏(leak)”到导出的API中。
对于公有类的成员,当访问级别从包级私有编程保护级别时,会大大增强可访问性。受保护的成员是类导出的API的一部分,必须永远得到支持。此外,导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺(第19项)。受保护的成员应该尽量少用( The need for protected members should be relatively rare)。
有一条关键规则限制了降低方法的可访问性的能力。如果方法重写了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别[JLS, 8.4.8.3]。这样对于确保任何可使用超类的实例的地方也都可以使用子类的实例是必要的(Liskov替换规则,参见第15项(the Liskov substitution principle, see Item 15))。如果违反此规则,编译器将在你尝试编译子类时生成错误消息。这条规则的一个特例是,如果一个类实现了接口,则接口中的所有类方法都必须在类中声明为public。
为了便于测试,你可以尝试着使类、接口或者成员变得更容易访问。这么做在一定程度上来说是好的。为了测试而将一个公有类的私有成员变成包级私有的没辙还可以接受,但是要将访问级别提高到更高,这就无法接受了。换句话说,为了测试,而将类、接口或成员作为包导出的API的一部分是不可接受的。幸运的是,也没必要这么做,因为可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有的元素。
实例字段决不能是公有的 (第16项)。如果一个实例字段不是final的,或者是一个指向可变对象的引用,那么通过将其设置为公有的,就放弃了对存储在这个字段的值进行限制的功能(you give up the ability to limit the values that can be stored in the field)。这意味着,你也放弃了强行限制这个字段不可变的能力。此外,当这个字段被修改的时候,你放弃了对它采取任何操作的能力。因此具有公共可变字段的类通常不是线程安全的。 即使字段是final的并且引用不可变的对象,当把这个字段变成公有的时候,也就放弃了使用一个内部数据代表一个不存在的字段的灵活性(you give up the flexibility to switch to a new internal data representation in which the field does not exist)。
同样的建议也适用于静态域,只是有一种例外情况。假设常量构成了类提供的整个抽象中的一部分,可以通过公有的静态final域来暴露这些常量。按照惯例,这些字段的名称由大写字母组成,单词由下划线分隔(第68项)。这些字段包含基本类型的值或对不可变对象的引用,这一点至关重要(第17项)。如果final域包含可变对象的引用,它便具有非final域的所有缺点。虽然引用本身不能被修改,但是它所引用的对象却可以被修改————这会导致灾难性的后果。
注意,长度非零的数组总是可变的,所以,类具有公有的静态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的源文件中)。 模块外部无法访问模块中未公开包的公共成员和受保护成员;在模块中,可访问性不受导出声明的影响。 使用模块系统,您可以在模块中的包之间共享类,而不会让整个世界都可以看见它们(Using the module system allows you to share classes among packages within a module without making them visible to the entire world)。未导出包中的公共类的public和protect成员产生两个隐式访问级别,这是正常公共级别和受保护级别的模块内类似物( which are intramodular analogues of the normal public and protected levels)。这种共享需求相对较少,通常可以通过重新安排(rearranging)包里面的类来消除。有限实用的模块不仅为典型的Java程序猿提供访问保护,而且其性质主要是提供咨询【功能】(Not only is the access protection afforded by modules of limited utility to the typical Java programmer, and largely advisory in nature)。为了利用它,你必须将软件包分组到模块中,在模块声明中明确其所有依赖项,重新编排你的源代码树(source tree),并采取特殊操作以适应模块内对非模块化软件包的任何访问[Reinhold, 3]。现在说模块是否会在JDK本身之外得到广泛使用还为时过早。与此同时,除非你有迫切需要,否则最好避免使用它们。
总而言之,您应该尽可能地减少程序元素的可访问性(在合理范围内)。在仔细设计了最小的公共API之后,您应该防止任何杂散(stray)类、接口或成员成为API的一部分。除了作为常量的公共静态final字段之外,公共类应该没有公共字段。确保公共的静态final字段引用的对象是不可变的。