Skip to content

Latest commit

 

History

History
85 lines (52 loc) · 11.1 KB

第 15 条:最小化类及其成员的可访问性.md

File metadata and controls

85 lines (52 loc) · 11.1 KB

第 15 条:最小化类及其成员的可访问性

区分模块设计得是否良好,最重要的因素就是,该模块是否对其他模块隐藏了其内部数据和其他实现细节。拥有良好设计的模块会隐藏所有内部实现细节,明确的将其 API 和具体实现细节分割开来。然后,模块直接只能通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作细节。这种概念称为信息隐藏(information hiding)或封装(encapsulation),是软件设计的基础性原则。

出于种种原因,信息隐藏非常重要,其中大部分原因是因为它解耦了组成系统的各个组件,允许各组件独立地进行开发、测试、优化、应用、理解和修改。由于各组件可以各自独立地开发,从而加快了系统开发。由于可以更快地理解组件并进行调试或替换,而不必担心损害其他组件,从而减轻了维护的负担。虽然信息隐藏本身不会带来良好性能,但它可以有效地对性能进行有些优化:一旦系统完成,分析并确定是哪个组件导致的性能问题时(第 67 条),这些问题组件可以在不影响其他组件的同时进行优化。在开发中,低耦合的组件往往在其他环境中也是可用的,信息隐藏提高了软件的重用性。最后,信息隐藏降低了开发大型系统的风险,因为即使系统开发失败,组件也可以独立地开发成功。

Java 在信息隐藏方面有很多工具。访问控制(access control )机制 [JSL,6.6] 明确规定了类、接口和成员的可访问性(accessibility)。实体的访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private, protected,public)决定的。这些修饰符的恰当使用对信息隐藏非常关键。

经验法则非常简单:将每个类或成员都尽可能的设置为不可访问。换句话说,使用与你正在编写的软件自身功能一致的尽可能低的访问级别。

对顶级(非嵌套的)类和接口而言,它们只有两种可能的访问级别:包级私有的(package-

private)和公有的(public)。如果使用 public 修饰符声明顶级类或接口,该类或接口就会是公有的;否则其将会默认为包级私有的。如果顶级类或接口可以定义为包级私有的,它就应该定义为包级私有的。通过定义类或接口为包级私有的,可以将其作为实现的一部分,而不是暴露出去的 API,也可以在后续版本中修改、替换或删除这些类和接口,而不必担心损害已有的客户端。但如果你定义类或接口为公有的,那就必须永远保证该类和接口的兼容性。

如果只有一个类使用包级私有的顶级类或接口,那么考虑将该顶级类定义为唯一使用它的那个类的私有静态嵌套类(第 24 条)。这样就将顶级类或接口的可访问性从包中的所有类降低到了唯一使用它的类。但是,相比降低包级私有的顶级类的可访问性,降低不必要公有类的可访问性要重要的多:公有类是包的 API 的一部分,而包级私有的顶级类已经是它的实现细节的一部分了。

对成员(域、方法、嵌套类和嵌套接口)而言,有四个可能的访问级别,这里按照可访问性的递增次序列出:

  • private——只有在声明该成员的顶级类内部才可以访问这个成员。
  • package-private——可以在声明该成员的类所在包的任何一个类中方为这个成员。从技术上讲,称这种访问级别为默认访问级别(default),如果没有为成员指定任何访问修饰符,就采用这个访问级别。
  • protected——可以在声明该成员的类的子类(但有一些限制 [ JSL, 6.6.2 ]),及声明该成员的类所在包的任何一个类中访问这个成员。
  • public——可任意访问这个成员。

谨慎设计类的公有 API 后,你应该将所有成员设置为私有的。仅当位于同一包内的其他类确实需要访问成员时,才应该去掉 private 修饰符,让该成员成为包级私有的。如果你发现你经常需要这样做,那么就得重新审查系统的设计,看看另一种分解能否产生更好的相互解耦的类。这就是说,私有的和包级私有的成员都是类实现细节的一部分,通常不会影响该类暴露出去的 API。然而,如果类实现 Serializable 接口(第 86 条第 87 条),这些域就可能会被“泄露(leak)”到导出的 API 中。

就公有类的成员来说,可访问性从包级私有到保护级别巨幅增加。受保护的成员是类所暴露 API 的一部分,必须永远维护。公有类的受保护成员也代表了该类对于某个实现细节的公开承诺(第 19 条)。受保护的成员应该尽量少用。

有一条关键规则限制了降低方法的可访问性的能力。如果一个方法覆盖了父类方法,那么在子类中该方法的可访问级别不能比父类中严苛 [ JSL, 8.4.8.3]。这样可以确保可使用父类实例的任何地方,都可以使用子类实例(Liskov substitution principle, 见[第 15 条][Item15])。如果违反了这条原则,在编译子类时,编译器会产生错误信息。关于这条规则有个特殊情况,如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须声明为公有的。

为了更好地测试代码,你可以让你的类、接口或成员变得更容易访问。这样做在一定程度上是可以的。为了测试,将公共类的私有成员改为包级私有是可以接受的,但决不允许可访问性再高任何一点。换句话说,不能为了测试将类、接口或成员定义为包导出的 API 。幸运的是,也没有必要这么做,因为测试可以只运行包的一部分来测试,从而能够访问它的包级私有的元素。

公有类的实例域绝不能是公有的 第 16 条)。如果实例域非 final 型,或该域是一个可变对象的引用,那么一旦将其定义为 public,就无法对存储在这个域中的值进行限制;这意味着也无法强制这个域不可变。同样,当域中的值发生改变时你将无可奈何,所以 包含公有可变域的类不是线程安全的。即使域是 final 型而且引用不可变的对象,将其设置为公有的,也失去了切换到字段不存在的新内部数据表示形式的灵活性。

同样的建议也适用于静态域,只是有一种例外情况。假设常量构成了类提供的整个抽象中的一部分,通过公有的静态 final 域可以暴露这些常量。按照惯例,这样的域一般都是由大写字母命名的,字母间用下划线隔开(第 68 条)。很重要的一点是,这些域包含初始值或指向不可变对象的引用。如果 final 域包含可变对象的引用,那么将具有非 final 域的所有缺点。虽然引用本身不能修改,但被引用的对象可以修改,一旦修改,会造成灾难性的后果。

注意非空数组一定是可变的,所以类不可以含有公有的静态 final 数组域,或返回公有的静态 final 数据域的访问方法。如果类含有这样的域或访问方法,客户端将可以修改数组的内容。这是一个很常见的导致安全漏洞的原因:

	//潜在的安全漏洞!
	public static final Thing[] VALUES = { ... };

一些 IDE 会生成返回私有数组域的引用的访问方法,这样也会导致同样的漏洞。有两种方法解决问题,一种是将公有数组修改为私有的,然后添加公有的不可变的 LIst:

	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 system,通常包含在名为 moudle-info.java 的源文件中)的一部分引入的。模块是一组包的集合,就像包是一组类的集合。模块可以通过它的模块声明(module declaration)中的导出声明(export declaration)导出一些包。模块中未导出包的公有的和受保护的成员在模块外是不可访问的;在模块内部,这些成员的访问性不受导出声明的影响。使用模块系统,可以在模块中的包之间共享类,而不必让它们对整个世界可见。未导出包中公共类的公有的和受保护的成员多出了两种隐含的访问级别,它们分别是普通和保护级别的模仿。这两种程度共享的需求相对较少,通常可以重新安排包中的类来消除这种需求。

不同于四种主要的访问级别,这两种基于模块的访问级别主要是咨询性的。如果将模块的 JAR 文件放在应用的类路径下而不是模块路径下,模块内的包将会表现出非模块化行为:无论模块是否导出这个包,包中公有类的所有公有和受保护成员都有正常的可访问性 [Reinhold, 1.2]。有一个地方严格执行新引入的访问级别,那就是 JDK 本身:Java 依赖中所有未导出的包在模块外都不可访问。

模块提供的访问保护功能有限,不仅对典型的Java程序员有用,而且基本上是建议性的;为了利用好 它们,必须将包分组到模块中,在模块声明中显式地声明所有依赖项,重新排列源树,并采取特殊操作来适应任何对模块内的非模块化包的访问[Reinhold, 3]。模块是否会广泛应用在 JDK 以外的地方,现在断言还为时尚早。同样,除非特别需要,不然最好避免使用这些工具。

总之,应该尽可能将减少程序元素的可访问性(理性的)。在谨慎设计最小公开化的 API 后,应该避免任何游离的类、接口或成员成为 API 的一部分。除了公有静态 final 域这一例外情况,所有公有类都不该含有公有域。并且要确保公有静态 final 域所引用的对象是不可变的。

翻译:Inger

校对:Inno