Effective Java---15 使类和成员的可访问性最小化

前言

我自己在平时的编程中其实并未太多思考类和成员的访问性,要么是private,或者就是public直接用起来,大家都知道Java中有四种访问级别,从小到大依次是private(类内可访问)、protected(同包下及子类可访问)、default(同包下类可访问,子类不能)、public(All),那我们为什么需要不同的访问修饰符呢?

信息隐藏

区分设计良好的组件与设计不佳的组件的最重要的依据是,它们关于隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件之间只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,也是软件设计的基本原则。

  1. 信息隐藏很重要主要是因为它可以实现系统组件的分离,允许每一个组件被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发,相比串行来说效率极大提高。
  2. 同时减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件,因为组件之间是相互隔离的,对外暴露接口保证通信。虽然信息隐藏本身并不会实现良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题,则可以优化这些组件,而不会影响别人的正确的组件。
  3. 信息隐藏增加了软件模块的重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的。最后,隐藏信息降低了构建大型系统的风险,即使某一个服务挂掉了,也不一定影响整个系统的运行。

访问控制机制

怎么实现信息隐藏呢?这就要用到我们最开始提到的访问控制修饰符了。

Java中主要实体成员无非就是类及对象、接口、变量及常量, 实体的可访问性取决于其声明的位置,以及声明中存在哪些访问修饰符(private,protected 和 public)。 正确使用这些修饰符对信息隐藏至关重要。

既然有四种访问级别,那设计者最初的目的肯定是希望开发人员能够关注到Java中实体的访问级别,即只给有需要访问的成员或者说开发人员希望有访问权限的成员访问权限,那就需要我们将每一个成员的可访问性最小化

经验法则很简单:让每个类或成员尽可能地不可访问。 换句话说,使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。

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

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

在仔细设计你的类的公共 API 时,首先考虑所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问私有成员时,需要删除私有修饰符,从而使成员包成为包级私有的(default)。 如果需要暴露的属性很多,那就说明类的设计并不合理,看可以分解成两个解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些字段可以「泄漏(leak)」到导出的 API 中,即可以直接访问到我们设计的“内部”。

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

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

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

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

同样的建议适用于静态字段,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 字段暴露常量。 按照惯例,这些字段的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些字段包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的字段具有非 final 字段的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。

请注意,非零长度的数组总是可变的,所以类具有公共静态 final 数组字段,或返回这样一个字段的访问器是错误的。 如果一个类有这样的字段或访问方法,客户端将能够修改数组的内容,VALUES是数组的引用是不变的。 这是安全漏洞的常见来源:

// 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 文件放在应用程序的类路径而不是其模块路径中,那么模块中的包将恢复为非模块化行为:包的公共类的所有公共类和受保护成员都具有其普通的可访问性,不管包是否由模块导出[Reinhold,1.2]。 新引入的访问级别严格执行的地方是 JDK 本身:Java 类库中未导出的包在模块之外真正无法访问。

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

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值