类和接口是Java程序设计语言的核心,它们也是Java语言的基本抽象单元。Java语言提供了许多强大的基本元素,供程序猿来设计类和接口。
要想区别一个设计良好的模块与一个设计不好的模块,最重要的因素是,这个模块对于外部其他模块而言,是否隐藏了内部的数据和其他的实现细节。一个设计良好的模块会隐藏所有的实现细节,把它的API与实现清晰的隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不需要知道其他模块内部的工作情况,也就是所谓的封装。
封装之所以非常重要有许多理由,其中大多数理由都源于这样一个事实:它可以有效地解除一个系统中各模块之间的耦合关系,使得这些模块可以被独立地开发、测试、优化、使用、理解和修改。这样可以加速系统开发的速度,因为这些模块可以被并行地开发。它也减轻了维护的负担,因为程序员很快就可以理解这些模块,并且在调试它们的时候可以不伤害其他的模块。虽然封装本身无论是对内还是对外,都不会带来更好的性能,但是它使得有效的性能调节成为可能。一旦一个系统已经完成,通过分析就可以知道哪些模块影响了系统的性能,那么这些模块可以被进一步优化,而不会影响到其他模块的正确性。封装可以提高软件的可重用性,因为单独的模块并不依赖于其他的模块,除了开发这些模块所使用的环境之外,它们在其他环境中往往是有用。最后,封装也降低了构建大型系统的风险;即使整个系统并不成功,这些独立模块也有可能是成功的。
Java程序设计语言提供了许多设施来帮助坐到信息隐藏。其中一个设施是访问控制机制,它决定了类、接口和成员的可访问性。
一个实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符公同决定的。正确地使用这些修饰符对于实现封装是非常关键的。
**经验表明,你应该尽可能地使每一个类或成员不被外界访问。**换句话说,你应该使用时最低可能的、并且与该软件的正确功能相一致的访问级别。
对于顶层的类和接口,它们只有两种可能的访问级别:包级私有的和公有的。如果你声明了一个具有public修饰符的顶层类或者接口,那么它是公有的;否则,它将是包级私有的。如果一个类或者接口能被做成包级私有的,那么它就应该被做成包级私有的。通过把一个类或接口做成包级私有的,它实际上成了这个包的实现的一部分,而不是该包导出API的一部分;并且,在以后的发行版本中,你可以对它进行修改、替换,或者去除,而无需担心会伤害到现有的客户。如果你把它做成公有的,你就有义务永远支持它,以保持兼容性。
如果一个包级私有的顶层类或接口只是在某一个类的内部被用到,那么你应该考虑使它成为后者的一个私有嵌套类。这样可以进一步降低它的可访问性,然而,这样做不像”使一个不必要的共公有类成为包级私有的类”那样重要,因为一个包级私有的类已经是这个包的实现的一部分,而不是其API的一部分。
对于成员有四种可能访问级别,下面按照可访问性递增的顺序列出来:
-
私有的——只有在声明该成员的顶层类内部才可以访问这个成员
-
包级私有的——声明该成员的包内部的任何类都可以访问这个成员。在技术上,它被称为”默认访问级别”,如果没有为成员指定访问修饰符的话,那么它就具有这样的访问级别。
-
受保护的——该成员声明所在类的子类可以访问这个成员,并且,该成员声明所在的包内部的任何类也可以访问这个成员。
-
公有的——任何地方都可以访问该成员。
当你仔细地设计了一个类的公有API之后,接下去应该把所有其他的成员都变成私有的。只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该去掉private修饰符,使该成员变成包级私有的。如果你发现自己经常要做这样的事情,那么你应该重新检查你的系统设计,看看是否另一种分解方案所得到的类具有更好的分离特性,彼此之间耦合度更小。可以这样说,私有成员和包级私有成员都是一个类实现中的一部分,并不会影响到其导出的API。然而,如果这些域所在类是实现了Serializable接口,那么这些域可能会被”泄露”其导出的API中。
对于公有类的成员,当访问级别从包级私有变成保护级别时,会出现可访问性的巨大增加。受保护成员是一个类导出API的一部分,必须永远被支持。更进一步,一个导出的类的每一个受保护成员代表了该类对于一个实现细节的公开承诺。受保护的成员应该尽量少用。
有一条规则使得你无法降低一个方法的可访问性。如果一个方法改写了超类中的一个方法,那么子类中该方法的访问级别低于超类中的访问级别时不允许的。这样可以确保子类实例可以被用在任何可使用超类的实例的场合。如果你违反了这条规则,那么当你试图编译该子类的时候,编译器会产生一条错误信息。这条规则的一种特殊情形是,如果一个类实现了一个接口,那么接口中所有的方法在这个类中都必须被声明为公有的。这是因为接口中所有方法都隐含着公有访问级别。
公有类应该尽可能地包含公有的域。如果一个域是非final的,或者是一个指向可变对象的final引用,那么你一旦使它成为公有的,就放弃了对存储在这个域中的值进行限制的能力;当这个域被修改的时候,你也失去了采取任何行动的能力。一个简单的后果是,包含公有可变域的类不是线程安全地的。即使一个域是final的,并没有指向任何一个可变对象,那么,一旦你把这个域变成公有的,也就放弃了“切换到一个新的内部数据表示”的灵活性。
对于“公有类不应该包含公有域”这条规则也有一个例外,通过公有的静态final域来暴露类的常量是允许的。按照惯例,这样域的名字由大写字母组成,单词之间用下划线隔开。很重要的一点是,这些域要么包含原语类型的值,要么包含指向非可变对象的引用。如果一个final域包含一个指向可变对象的引用,那么它具有非final域的所有缺点。虽然引用本身不能被修改,但是它引用的对象可以被修改——这会导致灾难性的后果。
注意,非零长度的数组总是可变的,所以**,具有公有的静态final数组域几乎总是错误的**。如果一个类包含这样的一个域,客户将能够修改数组中的内容。这是安全漏洞中的一个常见根源:
另一种办法是,如果你要求编译时的类型的安全性,并且愿意损失一点性能的话,你可以把公有的数组替换成一个公有的办法,它返回私有数组的一份拷贝:
总之,你应该总会可能地降低可访问性,再仔细设计了一个最小的公有API之后,应该防止把任何杂散的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类不应该包含公有域。并且确保公有静态final域所引用对象是不可变的。