Effective-java 3 中文翻译系列 (Item 15 最小化类和类成员的可访问性)

96
TryEnough
2018.06.01 15:36* 字数 2939

原文链接

文章也上传到的

github

(欢迎关注,欢迎大神提点。)


类和接口是Java语言的核心。他们是组成抽象的基本单元。Java语言提供了很多关于类和接口的特性,这一章节的内容帮助你使用这些特性来实现更加健壮,灵活的类和接口。

ITEM 15 最小化类和类成员的可访问性

区分组件好坏的一个重要条件是看它是否隐藏了自身内部的数据和实现。一个好的组件会隐藏它的所有实现细节,并将它自己的API和API的实现清晰的分离开来。然后组件间的通讯仅仅通过他们的API,并忽略彼此的内部实现。这被叫做隐藏或封装。这也是软件设计的基本原则。

在一个系统中的各个组件之间,隐藏组件各自的内部实现主要是为了解耦合。这样每个组件就可以单独进行开发、测试以及优化。各个组件之间的平行化开发也加快了系统的开发效率。并且维护起来也更容易,因为组件能够更加快速的调试、修改以及替换原来的实现。虽然隐藏实现信息本身不能造成高性能,但是它能影响到执行的性能:一旦一个系统开发完成,分析是那一部分模块造成的问题就变得可行了,那些有问题的模块组件可以在不影响其他组件的情况下进行优化。隐藏实现增加了软件的重用性,组件之间的低耦合通常被证明即使不在它们被开发出来的场景下也是可以被重用的。最后,隐藏实现减小了编译大型项目的风险,因为即使在系统不能正常使用情况下,被分离的组件也是可以被单独验证其正确性的。

Java有很多工具可以帮助实现隐藏信息。访问控制机制指的是类、接口或者成员的可访问性。这个可访问性指的是被(private、protected和public)中哪一个修饰词修饰。正确的使用这些修饰词是隐藏实现的必要手段。

经验法则很简单:尽量使每一个类和成员不可被访问。换句话说就是在保证你写软件正确的基础上使用最小的可访问等级。

对于顶层(非嵌套)的类和接口,只有两种可访问的等级package-private 和 public。如果你声明一个类或接口是public修饰的,它就是公开的。如果一个顶层的类或接口可以被设置成私有的,那就应该将其设置成私有的。通过将其私有化,你就可以随意的在将来的发行版里面修改、替换甚至删除它,而不用担心会影响到依赖于老版本实现的程序。但是如果你将它设置成public(公开的),那么你就有责任保持它的后续版本一直是可兼容的。

如果一个私有的顶层类或者接口仅仅在一个类中被使用,考虑将这个顶层的类或接口做成private static 私有的静态类内嵌到使用它的类里(Item 24),成为内部类,这样做可以减少了它的在同包中的可访问性。但是减少不必要的公共类的可访问性是更加重要的:因为公共类是API的一部分,而私有顶层类只是API实现的一部分,而API是可以随意被别人调用的。

对于成员(属性、方法、内嵌类和内嵌接口),有四种可以访问的等级,从上往下可访问性依次增加:

  • private-成员仅仅在被声明的类中可以被访问。
  • package-private-成员可以在同包的类中被访问。如果你没有指定任何可访问修饰语,默认就是这种访问等级(除了接口中的成员,接口中默认是public)。
  • protected-成员在声明它的类、子类以及同包中可以被访问。
  • public-任何地方都可以访问。

当设计完公共API之后,你应该让其他所有的成员都变成私有的。如果当同包中的其他类真的必须访问你的一个私有成员时,你应该将这个私有成员的修饰符从private改成 package-private以供它们调用。但是如果你发现你经常需要这么做,你应该重新审查你的系统设计,看是否可以通过分解类来更好的和其他的类解除耦合。然而,如果一个类实现了Serializable 接口,这些私有的域就会泄漏到API中(Items 86 和 87)。

一个protected类型的成员也是这个类导出的API的一部分,所以并且要永远支持。因为一个受保护的成员是这个类对公众承诺的实现细节(Item19)。但是一般情况下protected类型的成员是比较少见的。

有个约定是,你要尽可能的减少方法的可访问性。但如果一个方法重写父类的方法,它不能比父类有更小的可访问权限,必须保证父类可以被访问的地方子类也可以(传递规则)。如果你不遵守这个规则,在即编译子类的时候编译器就会生成一个错误信息。但是当一个类实现一个接口的时候,这些被实现的方法必须是public的。

你测试代码的时候,为了方便你可能会把你的类、接口或者成员的可访问性变成比他们需要的更大,这是一个可以加快速度的起点。但是把一个公共类的private成员变成package-private尚且可以接受,不可接受的是把它变成其他更高级的访问等级。换句话说就是,不可接受的就是为了方便测试把类、接口和成员由原来的private等级变成包的导出API等级。

类的实例字段最好不要设置成public的(Item16),如果一个public实例字段不是final的或者是一个mutable可变对象,这样你就对这个字段存储的值失去了控制。同时,当这个变量被修改的时候你也没办法做一些特殊动作了,所以类中的可变字段一般都不是线程安全的。即使是一个final和immutable的对象,假如被设置成public,同样也会失去在内部随意更换数据的灵活性。

同样的建议也适用于static字段,但有一个除外。当你通过public static final声明一个常量时,这个字段的命名应该是大写字母加下划线分割(Item68)。而且这些static字段应该包含原始值或对不可变对象的引用(Item17),如果包含了可变对象那就有了像非final字段一样的缺点:虽然不能修改这个final引用,但是却可以修改引用的对象,这具有灾难性的后果。

需要注意的是:一个长度不为零的数组总是可变(mutable)的,所以一个类有一个public static final array 字段或者geter方法(accessor)返回一个这样的字段都是不正确的。如果一个类有这样的字段或者这样的accessor,客户端(调用者)就可能会修改这个array的内容。这也是一个经常发生的安全漏洞:

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

你可以通过将数组私有化private或者添加一个public的不可变list来修复这个潜在的漏洞:

private static final Thing[]PRIVATE_VALUES = {...};
public static final List<Thing>VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

或者你可以使数组私有化然后添加一个public的方法返回对这个私有数组的拷贝:

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

你可以通过思考这些问题来选择到底使用什么方案:调用者更喜欢什么样的结果?哪种返回结果更加便利?哪种会有更好的表现等等。

在Java 9中,模块系统之间添加了两个隐式的访问级别。一个是包组,一堆包是一个模块,就类似于一个包里的类是一组一样。一个模块通常在它的module-info.java文件中通过模块声明导出它的一些包。在这个模块中未被导出的public和protect成员在其他模块中是不可见的,而在本模块中的可访问性不受影响。使用模块系统你就可以在本模块内共享一些类,而对其他模块进行隐藏。而在模块内没有被导出的包中public类的public和protect成员就类似于平常的public和protected访问等级一样。像这种需求一般比较少,而且通常可以通过调整包内类的顺序去除。

不像上面四种主要的访问等级,这两个基于模块的访问等级主要是建议。如果你把模块的JAR包放在了应用程序的类路径而不是模块路径:那么这个包里公共类的public和protected成员不管有没有被模块导出,就都像普通的可访问成员一样具有正常的可访问性了。这种新介绍的访问等级在JDK内部已经被严格的执行了:Java库中没有被导出的包在其他的包里面是不能被访问的。

对于传统的Java程序来说,模块之间提供的这种访问控制不仅仅实用上有限制,而且本质上目前也只是建议这么做。因为为了使用它,你必须分拆你的软件包成模块,然后很清晰的声明模块之间的相互依赖,重新调整你的源码树层级,并且当你的包内非模块化的代码被访问的时候才去合适的行为来适应这种调用。所以现在要求除了JDK之外的模块也实现这种访问控制还为时尚早。所以除非你有特殊需求,目前最好还是避免使用它们。

概括起来就是:在合理的范围内你应该尽可能减少程序的可访问等级。当仔细的设计完最小化公开的API之后,你应该放置任何零散的类、接口或者成员变成API的一部分。公共类除了pubic static final的常量(constant)属性外,不应该有任何其他的公共属性,而且要保证public static final的属性是不可变的。

JAVA
Web note ad 1