15. 使类可访问性最小化
尽可能降低程序元素的可访问性,尤其注意引用可变对象,尽量降低可访问性(尽管有时不容易),以保证线程安全性。
可变元素引用公有化合理方式是:
// 维护一个不可变长的备份
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
// 返回copy
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
16. 隐藏公有域
共有类永远不要暴露可变的域,可以通过get和set方法暴露接口。内部类暴露有时候便于使用。
17. 使可变性最小化
对象不可变会带来很多好处,其中最显而易见的是一致性——既然无法修改其状态,那它就是一致的,安全的。
让类变为不可变对象(尽可能)并不是很容易,涉及到规范,优点,实现,最佳实践。
不可变对象的规则
- 不要提供任何修改状态的方法。
- 保证类不会被拓展。子类可以通过屏蔽的方式,提供可改变的状态,因而应该防止子类化。方法有两个,final类 or 私有化构造器。
- 声明所有域为final和private,final确保本身不被篡改,private隐藏域,确保域引用的对象不被篡改。
- 确保对于任何可变组件访问都是互斥的。需要返回内部域的时候,可以复制一份。
不过事实上,这是比较严格的要求,在实践中,我们事实上是:保证没有一个方法能够对对象产生外部可见的改变。举个例子,在计算hashCode的时候,为了效率,我们通常会在对象中缓存hashCode,虽然状态(hashCode)改变了,当却对外不可见。
不可变对象的好处
不可变对象的好处都有什么,为什么要尽可能使可变性最小化。
- 简单。状态只有一个,通常在初始化之后就不需要额外的逻辑进行复杂维护。
- 线程安全,不要求同步。关键在于,它的状态就不会改变,也就没有所谓的一致性问题。
- 自由共享。还记得静态工厂中缓存优化吗,不可变方法因为不可变,所以可以安全的进行缓存共享的改进。
- 不可变对象共享内部数据。
public BigInteger negate() {
// mag为内部数组,不可变,可以在两个不同对象中共享
return new BigInteger(this.mag, -this.signum);
}
- 安全地作为其他对象的构件。作为其他类的组件的时候无需担心状态的问题
- 无偿提供一致性,原子性。
性能问题
然后,事实上,不可变对象的唯一的问题是性能——每次状态改变都需要多一个copy。比如,你使用一个几百万位的大整数做大量运算的时候,就会生成大量的copy,影响性能,这非常好理解。
实现方式
// 声明final避免被子类化
public final class Complex {
// final 和 private
private final double re;
private final double im;
// 构造器中初始化状态
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// 返回copy
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
防止子类化,final class是一种方式,另一种更好的方式是静态工厂方法。直接私有化构造器防止子类化,静态工厂方法还可以提供灵活性,缓存优化等等。
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
}
过于性能问题的解决思路。
方法一,如上述提到的大整数类大量运算的情况,可以通过基本类型对其中运算进行优化。
方法二,提供可变配套类,解决效率问题,尽管可变配套类的使用需要注意更多问题,但这些可以通过封装隐藏细节,提供安全性。典型的例子是String的可变配套类StringBuilder。
最佳实践
- 除非有充足理由,否则类应该就是不可变的。我们也知道了,不可变类有诸多好处。
- 除非有充足理由,否则域应该是private和final
- 尽可能保证只在构造器中初始化状态,不要提供reset的方法,这种优化没有必要。例如CountDownLatch,它虽然是可变类(猜测是内部资源状态改变),但是它不提供任何重置资源的手段。
18. 复合优先于继承
继承是实现代码重用的强大手段,但是继承会破坏封装性,也会带来诸多坏处。理解“复合优先于继承”为什么更好,先来理解继承为什么不好。
继承的缺陷
- 父类拓展容易破坏子类的规约。本质原因是子类对于父类的约束实际上仅仅通过覆盖来实现的,如果父类拓展了新方法子类未覆盖,则新方法可能破坏子类的规约。
- 继承会暴露过多父类的细节。客户端可以通过父类引用直接访问内部细节。例如JDK,Stack继承于Vector,但这是不合理的,使用Vector引用我可以访问到Stack的底层数组。这一条实际上和第一条是一样的,这种方式可以破坏子类预设的规约。同时,如果父类API本身有缺陷,这些缺陷也会一并继承。
- 覆盖和动态分派引起一些未预期的错误。
正因为上述缺点,在大部分情况下,复合都要优先考虑,它更加健壮,灵活,功能也更加强大。但继承也不是完全不能使用:
只有当子类真正是超类的子类型时,才适用于继承,这种关系是is-a。反例就是Stack的实现,Stack并不是一种Vector,所以更好的实现方式是复合,而不是继承。
19. 为继承而设计的类的规则
特别地,编写为继承而设计的类的时候,需要注意:
- 提供文档,说明调用了那些覆盖方法(自调用),会造成什么影响,有什么约束。特别是制定的规则和约束,需要通过大量测试确保周全,因为这意味这之后的所有子类都需要遵守
- 构造器,clone,序列化方法,绝对不能调用可覆盖的方法。因为子类初始化之前会调用父类初始化,如果调用被子类覆盖的方法,有可能使用到子类未初始化的域,这时候会造成错误或异常。
- 提供protected方法,为子类化提供合适的工具。有些实现不放心子类实现,也为了便于子类化,很多时候可以编写protected方法,给子类提供复用。protected的原因,猜测是限制 父类.method()这种情况,绕过子类的约束直接修改内部。
总而言之,如果不是有必要,那么少用继承,如果不是有必要,那么使用继承的时候不要调用任何可覆盖的方法,这可以保证避免自调用未来某天给程序带来“惊喜”。
20. 接口优于抽象类
由于Java是单继承的语言,所以理所当然,接口之于抽象类,体现的就是灵活性,同时回避继承的种种问题。
- mixin是理想的选择。mixin是指类除了实现基本的父类外,实现mixin接口,混合使用,这通常是一种最佳实践。
- 接口支持多继承,合并成大接口。
- 接口其实可以避免组合爆炸的现象。——你可以利用组合来实现接口的功能,但是这样依赖,你不得不编写n个基本组件,以及可能形成2^n中组合方式,尽管这些类的功能,形式基本一样
- 为重要的接口提供骨架类实现。接口用于定义基本方法,以及有时候可能提供基本实现。但这些通常无法满足基本父类的抽象实现,这时候可以编写抽象骨架类——编写抽象父类,实现接口中的可复用方法,或提供缺省方法。方便子类化
21. 接口缺省方法设计需要谨慎
JDK 1.8提供接口的默认方法的实现,方便了接口实现以及lambda函数式接口的实现。但这实现实际上是注入实现类中的,它在实现的时候并不知道实现类的情况,因而可能会在后期引起一些问题。
22. 接口只用于定义类型
主要是有些实现喜欢用接口来定义常量,导出常量。下面来分析一下这个问题:
// Constant interface antipattern - do not use!
public interface PhysicalConstants {
// jdk 1.7之后允许数字字面量添加_,没有任何影响,增加可读性
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
常量接口的缺点
很多时候为什么好需要为什么不好来定义,所以知道为什么不好很重要。
- 泄漏实现细节到导出API。实现常量接口,从而使用这些常量,但问题在于,内部使用什么东西都是细节实现问题,这种形式,使得这些实现细节都会泄漏到API中。
- 它还代表一种承诺:即使未来的版本中不再需要使用这些常量,它依然需要实现这些老接口以保证兼容性(接口可以看作定义类型,不能随意取消)。
导出常量的最佳实践
- 与紧密关联的类或接口绑定。例如Integer.MAX_VALUE
- 使用枚举类型
- 常量类要保证不可实例化,例如私有化构造器抛出异常。
- 使用静态导入避免大量类名修饰常量。
23. 层次化优于标签类
应该使用层次化(继承)来取代标签类。所谓的标签类就是指:
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
}
- 冗长,可读性差,内存浪费,效率低下
- 容易出错
24. 静态成员类优于非静态成员类
书中建议如果声明的成员类不要求访问外部类的成员,那么就应该始终把static加上。
主要还是理解为什么静态成员类优于非静态成员类:
核心原因只有一条,静态成员类没有指向外围类的引用。
- 不必要的消耗。这种引用其实增加了不必要的消耗。这种有时候并不像看起来那么微弱,例如map中如果Entry的内部类声明使用非静态内部类,那么每个entry就都持有对外部类的引用,这是没有必要的。
- 内存泄漏。更重要的是,它妨碍了外部类对象的回收,有可能造成内存泄漏。
https://www.yuque.com/kdlin/lioesk/zhr3x0
25. 限制源文件为单个顶级类
Java中允许在一个源文件中添加多个顶级类。
但因为不同源文件中可以存在重复的类名,这时候选择顺序是根据编译顺序决定的,这是我们无法接受的。所以建议一个源文件只写一个顶级类。