Walt You - 行是知之始

《Effective Java》学习日志(三)15:最大限度地减少类和成员的可访问性

2018-08-28

一个能区分组件是好是坏的重要因素,就是看这个组件隐藏其内部数据以及其他组件的实现细节的程度。 精心设计的组件隐藏其所有实现细节,将其API与其实现完全分离。 组件然后只通过他们的API进行通信,并且忘记了彼此的内部工作。 这种被称为信息隐藏或封装(information hiding or encapsulation)的概念是软件设计的基本原则。


学习资料主要参考: 《Effective Java Third Edition》,作者:Joshua Bloch



信息隐藏

信息隐藏很重要,原因很多,其中大部分原因在于它将构成系统的组件分离,允许它们单独开发,测试,优化,使用,理解和修改。 这可以加速系统开发,因为组件可以并行开发。 它减轻了维护的负担,因为可以更快地理解组件并进行调试或更换,而不必担心会损害其他组件。 虽然信息隐藏本身并不会导致良好的性能,但它可以实现有效的性能调整: 一旦系统完成并且分析确定哪些组件导致性能问题(Item 67),那些组件可以在不影响其他组件正确性的情况下进行优化。 信息隐藏增加了软件的重用,因为松耦合的组件,在除了开发之外的环境中,经常也是有用的。 最后,信息隐藏降低了构建大型系统的风险,因为即使整个系统没有成功,单个组件也可以成功。

信息隐藏工具

Java有许多辅助信息隐藏的工具。 访问控制机制指定类,接口和成员的可访问性。 实体的可访问性由其声明的位置确定,并且声明中存在访问修饰符(private , protected , public)的位置(如果有)。 正确使用这些修饰符对于信息隐藏至关重要。

经验法则很简单:让每个类或成员尽可能无法访问。 换句话说,在保证软件的正常运行时,保持最低的访问级别。

对于顶层(非嵌套的)类和接口,只有两个可能的访问级别: 包级私有(package-private)和公共的(public)。 如果你使用public修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。 如果一个顶层类或接口可以被做为包级私有,那么它就应该这样被设置。 通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。 如果你把它公开,你就有义务永远地支持它,以保持兼容性。

如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类(Item 24)。 这将它的可访问性从包级的所有类减少到使用它的一个类。 但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的API的一部分,而包级私有的顶级类已经是这个包实现的一部分了。

对于成员(属性、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:

  • private: 该成员只能在声明它的顶级类内访问。
  • package-private: 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符(接口成员除外,它默认是公共的),这是默认访问级别。
  • protected: 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类。
  • public: 该成员可以从任何地方被访问。

在仔细设计你的类的公共API之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的API。 但是,如果类实现Serializable接口(Item 86和87),则这些属性可以“泄漏(leak)”到导出的API中。

对于公共类的成员,当访问级别从包私有到受保护级时,可访问性会大大增加。 受保护(protected)的成员是类导出的API的一部分,并且必须永远支持。 此外,导出类的受保护成员表示对实现细节的公开承诺(Item 19)。 对受保护成员的需求应该相对较少。

有一个关键的规则限制了你减少方法访问性的能力。 如果一个方法重写一个超类方法,那么它在子类中的访问级别就不能低于父类中的访问级别。 这对于确保子类的实例在父类的实例可用的地方是可用的(Liskov替换原则,见 Item 15)是必要的。 如果违反此规则,编译器将在尝试编译子类时生成错误消息。 这个规则的一个特例是,如果一个类实现了一个接口,那么接口中的所有类方法都必须在该类中声明为public。

为了便于测试你的代码,你可能会想要让一个类,接口或者成员更容易被访问。 这没问题。 为了测试将公共类的私有成员指定为包级私有是可以接受的,但是提高到更高的访问级别却是不可接受的。 换句话说,将类,接口或成员作为包级导出的API的一部分来促进测试是不可接受的。 幸运的是,这不是必须的,因为测试可以作为被测试包的一部分运行,从而获得对包私有元素的访问。

公共类的实例属性很少公开(Item 16)。 如果一个实例属性是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在属性中的值的能力。 这意味着你放弃了执行涉及该属性的不变量的能力。 另外,当属性被修改时,就放弃了采取任何操作的能力,因此公共可变属性的类通常不是线程安全的。 即使属性是 final 的,并且引用了一个不可变的对象,通过使它公开,你就放弃切换到不存在属性的新的内部数据表示的灵活性。

同样的建议适用于静态属性,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过public static final属性暴露常量。 按照惯例,这些属性的名字由大写字母组成,字母用下划线分隔(Item 68)。 很重要的一点是,这些属性包含基本类型的值或对不可变对象的引用(Item 17)。 包含对可变对象的引用的属性具有非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,并添加一个返回私有数组拷贝的公共方法:

private static final Thing[] PRIVATE_VALUES = { ... };

public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

要在这些方法之间进行选择,请考虑客户端可能如何处理返回的结果。 哪种返回类型会更方便? 哪个会更好的表现?

在Java 9中,作为模块系统(module system)的一部分引入了两个额外的隐式访问级别。 模块包含一组包,就像一个包包含一组类一样。模块可以通过模块声明中的导出(export)声明显式地导出某些包(这是module-info.java的源文件中包含的约定)。 模块中的未导出包的公共和受保护成员在模块之外是不可访问的;在模块中,可访问性不受导出(export)声明的影响。 使用模块系统允许你在模块之间共享类,而不让它们对整个系统可见。 在未导出的包中,公共和受保护的公共类的成员会产生两个隐式访问级别,这是普通公共和受保护级别的内部类似的情况。 这种共享的需求是相对少见的,并且可以通过重新安排包中的类来消除。

与四个主要访问级别不同,这两个基于模块的级别主要是建议(advisory)。 如果将模块的JAR文件放在应用程序的类路径而不是其模块路径中,那么模块中的包将恢复为非模块化行为: 包的公共类的所有公共类和受保护成员都具有其普通的可访问性,不管包是否由模块导出。 新引入的访问级别严格执行的地方是JDK本身:Java类库中未导出的包在模块之外真正无法访问。

对于典型的Java程序员来说,不仅程序模块所提供的访问保护存在局限性,而且在本质上是很大程度上建议性的; 为了利用它,你必须把你的包组合成模块,在模块声明中明确所有的依赖关系,重新安排你的源码树层级,并采取特殊的行动来适应你的模块内任何对非模块化包的访问。 现在说模块是否会在JDK之外得到广泛的使用还为时尚早。 与此同时,除非你有迫切的需要,否则似乎最好避免它们。

总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共API之后,你应该防止任何散乱的类,接口或成员成为API的一部分。 除了作为常量的公共静态final属性之外,公共类不应该有公共属性。 确保public static final属性引用的对象是不可变的。


下一篇 Hbase 介绍

Content