Effective Java:Ch4_Class:Item13_最小化类及其成员的可访问性
要区别一个模块是否设计良好,最重要的因素是,对于其他模块而言该模块隐藏其内部数据和其他实现细节的程度。设计良好的模块应该隐藏所有实现细节,将API与其实现清晰地隔离开来。这样,模块之间通过他们的API进行通信,而不必知道其他模块的内部工作情况。这个概念被称为信息隐藏(information hiding)或封装(encapsulation),是软件涉及的基本原则之一。
信息隐藏之所以重要有许多原因,大多数原因都源于这样一个事实:它可以将组成系统的各个模块解耦,使得这些模块可以独立地开发、测试、优化、使用、理解和修改。1、信息隐藏可以加速系统开发,因为这些模块可以并行开发。2、信息隐藏可以减轻维护负担,因为可以更快地理解这些模块,在调试时不用担心影响其他模块。3、虽然信息隐藏不会提高性能,但是他可以有效地调节性能:一旦完成了一个系统,并且通过婆媳确定了哪些模块导致性能问题,那么就能优化这些模块而无需担心影响其他模块。4、信息隐藏可以增加软件重用性,因为模块间并不紧密关联,模块除了在开发过程中的环境中有用之外,在其他环境中通常也有用。5、最后,信息隐藏减少了构建大型系统的风险,因为即便系统不可用,但这些独立的模块却可能是有用的。
Java提供了许多机制来帮助实现信息隐藏。访问控制机制可以指定类、接口及其成员的可访问性。实体的可见性是由该实体声明所在的位置,以及声明的访问修饰符决定的。正确地使用这些访问修饰符对于信息隐藏是及其关键的。
第一规则很简单:使类及其成员尽可能地不被访问。也就是说,在保证功能的情况下,使用最低的访问级别。
顶级类
对于顶级(非嵌套)的类和接口,只有两种可能的访问级别:package-private、public。如果一个顶级类或接口可以设置成包级私有,那就应该设成包级私有。这样它就成了实现的一部分而不是API的一部分,你可以对它修改、替换,或者在以后的版本中删除它,而不用担心影响已有的客户端。而如果你把它设成public,你就有义务永远支持它,以保证兼容性。
类成员
如果包级私有的顶级类(接口)仅仅只被一个类使用,那就考虑将这个顶级类写成私有嵌套类。这样可以把可访问范围进一步缩小,从包中的所有类缩小到使用它的那个类。然而,降低不必要的public类的访问性,比降低包级私有类的访问性要重要得多,因为public类是API的一部分,而包级私有类只是实现的一部分。
对于类成员(字段、方法、嵌套类、嵌套接口),则有四种可能的访问级别,按照可访问性递增顺序罗列如下:
- private——成员只能被声明它的顶级类访问。
- package-private——成员能被声明该成员的包中的任何类访问。也被称为default访问级别。
- protected——成员能被声明它的类的子类访问,也能被声明该成员的包中的任何类访问。
- public——成员能被所有类访问。
【private】【package-private】
当仔细设计好类的公共API后,你应该把所有其他成员设为private。只有当同一个包中的其他类确实需要访问该成员时,才移掉private修饰符,使该成员变成package-private。如果你经常这么做的话,你就应当重新检查你的系统设计,看看是否有另一种分解方案得到的类,与其他类的耦合度会更小。私有成员和包级私有成员都是类的实现的一部分,一般不会影响类的导出API。然而,如果类实现了Serializable,则这些成员可能会泄漏到导出API中。
【protected】
对于public类的成员,当把访问级别从package-private改为protected后,其可访问性会大大增加。protected成员是该类的导出API的一部分,并且应当永远被支持。导出类的protected成员也代表了对实现细节的公开承诺。protected成员应当少用。
有一个规则限制了你降低方法访问级别的能力:如果子类重写父类的一个方法,则子类方法的访问级别不能被父类方法低。这可以确保子类在父类出现的任何地方都能适用。如果违反了这条规则,编译器会产生一个错误信息。这个规则的一个特殊情形是,如果一个类实现一个接口,则接口中的所有方法在该类中都必须为public。这是因为接口中的所有成员都隐含着public访问级别。
为了方便测试,你可能想扩大类、接口、成员的可访问性。这在一定程度上是可以的,我们可以接受将public类的private成员变成package-private,以便来测试这个成员,但是如果扩大到更高的可访问性就不可接受了。换言之,我们不能接受仅仅为了方便测试就将类、接口、成员变成导出API的一部分。幸运的是,也不必要这么做,因为测试用例可以作为待测包的一部分来运行,这样就可以访问其package-private的成员。——如果测试用例在其他包里,怎么办?
变量
【实例变量】永远不能为public。如果实例变量不是final的,或者虽然是final,但是指向一个可变对象;如果将该实例变量设为public,则你就放弃了限制该变量取值的能力。这意味着你同时放弃了对该变量进行约束的能力、放弃了当该变量被修改时采取必要措施的能力,所以拥有public可变字段的类不是线程安全的。
即使变量是final的,并指向一个不可变对象,如果将该变量设为public,你也就放弃了将该域切换为一个新的内部数据表示的灵活性。
同样的建议也适用于【静态变量】,除了一种例外情况。你可以使用public static final变量来暴露一个常量,可以假定这个常量是该类的抽象的一部分。按惯例,这种变量名称必须大写,单词间用下划线分开。关键的一点是,这种变量要么指向基本类型,要么指向不可变对象。如果一个final变量指向一个可变对象,那么它就有非final变量的所有缺点。虽然引用不可修改,但引用的对象是可以修改的,这会导致灾难性的后果。
要注意长度非零的数组总是可变的,所以类中不能有public static final的【数组变量】,也不能有返回这种变量的方法。如果一个类中包含这种变量或方法,则客户端就能够修改数组的内容。这是安全漏洞的一个常见根源。
// Potential security hole! public static final Thing[] VALUES = {...};
要注意,许多IDE会自动生成访问方法,返回指向private数组变量的引用,这就会导致上述问题。修正这个问题有两种方法:
1)将public的数组设为private,并添加一个public的不可变list:
private static final Thing[] PRIVATE_VALUES = {...}; public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
2)或者可以将数组设为private,并添加一个public方法来返回这个private数组的拷贝:
private static final Thing[] PRIVATE_VALUES = {...}; public static final Thing[] values() { return PRIVATE_VALUES.clone(); }
要在这两种方法间进行选择时,要考虑客户端可能怎么处理这个结果。那种返回类型更加方便?那种性能更好?
总之,
- 你应该尽可能减小可访问性。
- 在仔细设计一个最小的public API后,应该防止把不必要的类、接口、成员变成API的一部分。public类中,
- 除了public static final变量,不应该包含任何public变量。
- 确保public static final域引用的对象是不可变的。