Effective Java(3rd)-Item15 最大限度减少类和成员的访问性

  最重要的因素区别一个好的设计组件和不好的设计组件是组件隐藏了它的内部数据和其他组件的实现细节。好的设计组件隐藏了它的所有实现,将它的API和实现完全分离。组件之间只通过API进行通信,并且不清楚各自的内部工作。这个概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。
  信息隐藏很重要,有很多原因,其中很大部分原因是它将构成系统的组件解耦,允许它们单独开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以被并行开发。它减轻了维护的负担,因为组件可以被更快理解,调试或更换而不必担心损害其他组件,虽然信息隐藏本身不会造成好的性能,但是它实现了有效的性能调整:一旦系统完成并分析确定那些组件造成了性能问题(item67),这些组件可以在不影响其他正常组件的情况下进行优化。信息隐藏增加了软件重用,因为没有紧密耦合的组件在其他环境中是有用的,除了它们开发的那些。最后,信息隐藏减少了构建大型系统的风险,因为即使系统没有构建好,独立的组件也能成功。
  Java有许多设备来辅助信息隐藏。 控制访问机制[JLS, 6.6]指定了类,接口和成员变量的可访问性。实体的可访问性由其声明的位置确定,如果有,声明存在访问修饰符(private, protected, 和 public)。正确使用这些修饰符是信息隐藏必不可少的。
  经验法则很简单:让每个类或成员尽可能无法访问。换句话说,在编写软件正常运行下,尽可能使用最低可访问级别。
  对于顶级(非嵌套)类和接口,只有两种可能的访问级别:package-private 和public。如果你用public修饰符声明顶级类或接口,它将是公共的;否则,它将是包私有的。如果一个定继类或接口可以变为包私有的,那它就应该是。通过使其成为包私有,你可以使它称为实现的一部分而不是出口API,你可以在后续版本中修改它,替换他或消除它,而不用担心影响现有的客户端。如果你使其public,你有义务一直支持它以保留兼容性。
  如果只有一个类使用包私有的顶级类或接口,考虑将顶级类成为使用它的唯一类的私有静态嵌套类(item24) 。这降低了所有类到它的一个类的可访问性。但是比包私有顶级类更重要的是降低public类的可访问性:public类是包API的一部分,而包私有类早已是它实现的一部分。
  对于成员变量(字段,方法,嵌套类和嵌套接口),有四种可访问等级,如下按照增加访问性的顺序列出:
private—成员变量只能从声明它的顶级类中访问。
package-private—成本变量能在声明的包中的任意类访问。技术上成为默认访问,如果你没有加上访问修饰符,这就是默认访问级别(除了默认的为public的接口除外)。
protected—成员变量能从声明的类和子类中访问(有一些限制[JLS, 6.6.2])以及声明的类的包中的任何类都能访问。
public—任何地方任何类都能访问。
  仔细设计你的类的公共API后,你的反应应该是让所有其他成员都为private。只有当同一个报另一个类真的需要访问成为,你应该去掉private修饰符,使得成员变得package-private。如果你发现你自己经常这么做,你应该重新检查你的系统的设计,看看另一个分解是否产生更好彼此分离的类。也就是说,private和package-private成员都是类实现的一部分,但通常不会影响出口API。然而,如果类实现了Serializable,这些字段可能会“泄漏”到出口API(item86 和item87 )。
  对于public类的成员,当访问等级从package-private 到 protected时,可访问性大大增加。受保护的成员是类出口API的一部分,必须永久支持。此外,出口类的protected成员表示对实现细节的公开承诺(item19)。对protected成员的需求相对比较罕见。
  
  有一个关键规则限制你降低方法的可访问性的能力。如果一个方法覆写了超类的方法,它不能在子类中比父类的访问性低[JLS, 8.4.8.3]。这是必要的,因为这样可以确保子类实例在父类实例中的任何地方都是可用的(里氏替换原则),如果你违反了这个规则,当你尝试编译子类时,编译器将生成错误消息。这个规则的特例是如果一个类实现了一个接口,所有这个类的方法都必须在类中声明为public。
  为了方便测试你的代码,除了其他必要的,你可能倾向使类,接口或成员拥有更多访问性。这一点很好。为了对其测试,可以将public类的private成员package-private,但是提高可访问性是不可接受的。换句话说,为了便于测试,将类,接口或成员作为出口API的一部分是不可接受的。幸运的是,这也没有必要,因为测试可以作为被被测试包的一部分运行,从而获得对其package-private元素的访问。
  public类的实例字段应该几乎不为public(item16) .如果一个实例字段是nonfinal的或它是一个可变对象的引用,使其public,你就放弃了可以在字段中存储值得能力了。这意味着你放弃强制执行涉及该字段的不变量的功能。此外,你放弃在修改字段时采取任何操作的能力。所有具有public可变字段的类通常不是线程安全的。即使字段是final的,引用了一个不可变对象,让其oublic,你就放弃切换到不存在该字段的新内部数据表示的灵活性。
  同样的建议适用于静态字段,但有一个例外。你 可以通过public static final字段暴露常量,假设常量形成了类提供的抽象组成部分。按照惯例,这些字段的名字由大写字母组成,单词用下划线分割(item68) 。这些字段包含原始值或不可变对象的引用至关重要(item17) 。
  包含对可变对象的引用的字段具有nonfinal字段的所有缺点。虽然无法修改引用,但是修改引用的对象会带来灾难性的结果。
  请注意,非零长度数组总是可变的,所以类拥有一个public static final 数组字段或但会此类字段的访问器是错误的。如果一个类拥有这样的一个字段或访问器,客户端将有可能修改数组的内容,这就是安全漏洞的常见原因:

image.png

  注意到一些IDE生成的访问器会返回对自由数组字段的引用,从而导致这个问题。有两种方法来解决这个问题。你可以使public数组设为private,并添加public不可变列表:


image.png

  或者,你可以将数组设为private,添加public方法返回这个private数组的副本。


Effective Java(3rd)-Item15 最大限度减少类和成员的访问性_第1张图片
image.png

  在这些选择方案中选择,请考虑客户端可能会对结果做什么。哪种返回类型更方便?哪个会带来更好的性能?
  从Java9开始,作为模块系统的一部分引入了两个额外的隐式访问级别。模块是一组包,就好像一个包是一组类一样。一个模块可以在模块声明中通过导出声明显式导出一些包(按照惯例包含在名为module-info.java的源文件中),未公开包的public和protected成员在模块中不受导出声明的影响,在模块外无法访问。使用模块系统允许你在包中分享类,而不会对整个世界可见。未导出包的public类的public和protected成员产生两个隐式访问级别,它们是正常公共级别和受保护级别的模块内类似物。这种共享的需求相对较少,通常可以通过重新安排包中的类可以消除。
  不像四个主要访问级别,这两个基于模块的等级主要是建议性的。如果你将模块的JAR包放在应用程序的类路径上而不是它的模块路径上,模块中的包重新恢复非模块化的行为:包的共用类的所有公共和受保护成员都具有正常的可访问性,无论软件包是否由模块导出[Reinhold, 1.2]。严格执行新引入的访问级别的地方是JDK本身:Java库中未导出的包在其模块之外是真正无法访问的。
  对于典型的Java程序员而言,有限的实用程序模块不仅提供访问保护,而且主要是咨询性的;为了利用它,你必须将你的包分组到模块中,在模块声明中明确其所有依赖项,重新排列源代码树,并采取特殊操作以适应模块内对非模块化软件包的任何访问 [Reinhold, 3].现在说模块是否会在JDK本身之外得到广泛使用还为时过早。同时,除非你有迫切需要,你最好避免使用它们。
  总而言之,你应该减少程序元素的可访问性,尽可能地(在合理范围内)。在仔细设计最小公共API之后,你应该阻止任何杂散类,接口或成员成为API的一部分。除了作为常量的公共静态final字段之外,public类应该没有public字段。确保public static final字段引用的对象是不可变的。
本文写于2019.3.19,历时1天

你可能感兴趣的:(Effective Java(3rd)-Item15 最大限度减少类和成员的访问性)