第14条:在公有类中使用访问方法而非公有域
一个类应该将自身的数据私有化,并向外暴露 getter/setter 方法,使外部类通过其来 访问/修改 数据,这是为了保留将来改变这个类内部表示的灵活性。
但如果这个域是不可变的,那么直接将其暴露在外的危害就会比较小一些,但是在读取这个数据时,依然无法在类中采取辅助操作。
第15条:使可变性最小化
不可变类是指该类每个实例中包含的所有信息都必须在创建时就提供,而且在对象的整个生命周期中保持不变。
不变类有许多优点:
- 不可变对象比较简单
- 不可变对象本质就是线程安全的
- 不仅可以共享不可变对象,甚至可以共享其中的信息。
不可变类真正唯一的缺点是:对于每个不同的值都需要一个单独的对象,即是每次对变量的修改都需要创建一个全新的对象,这会对性能造成影响,String就是一个很典型的例子。
为了使类成为不变类,要遵循以下五条法则:
- 不要提供任何会修改对象状态的方法。
- 保证类不会被扩展
- 使所有域都成为私有的
- 使所有的域都是final的
- 确保对于任何可变组件的互斥访问:如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,而且不能用客户端提供的对象引用来初始化这样的域。
但是在实际使用中,为了提高性能,可以稍微放松一些规则。对于某些而言,完全的不可变性是不切实际的,在这种时候,应该尽可能的限制它的可变性。
第16条:复合优先于继承
相较于方法调用,继承打破了封装性,因为子类的部分功能会依赖于超类的具体实现,即使子类的方法没有任何的改变,也有可能因为超类的改变而变化。
例如我们编写了一个继承于HashSet类的MyHashSet类,并在其中加入一个变量来统计这个集合自创建以来添加过多少个元素:
public class MyHashSet<E> extends HashSet<E> {
private int addCount = 0;
.......
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
@Override
public boolean add(E e){
addCount++;
return super.add(e);
}
}
我们使用addAll方法向这个集合中添加三个元素,之后观察addCount的值,会发现addCount的值增加了6而不是期望的3。这是因为addAll方法是基于Add方法来实现的,在调用addAll方法增加了一次addCount的值后会调用add方法再次增加addCount的值,导致每一个元素都被计算了两次。因为这是具体的实现细节,不在文档中说明是合理的,所以对每一个类的继承都会存在这样的隐患。
还有一个原因,在项目后序的更新中,可能会对一个实现增加新的方法,而如果子类没有做出相应的处理,那么当客户端调用新增的方法时就可能出现问题。
但是如果使用复合的方式来实现,就不会遇到上述的问题。“复合”就是在新的类中增加一个新的域来应用现有类的一个实例,这样现有类便成为了新类的一个组件。还是以MyHashSet举例:
public class MyHashSet<E> {
private HashSet hashSet;
private int addCount = 0;
......
public boolean addAll(Collect<? extends E> c) {
addcount += c.size();
return hashSet.addAll(c);
}
public boolean add(E e){
addCount++;
return hashSet.add(e);
}
}
因此,只有在子类与超类的关系确实是“is-a”的关系时,才适合使用集成,否则,使用复合会更加合适。
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
在上一条中已经说明了为什么应该尽量的使用复合而非继承,那么如果一个类需要使用继承应该如何去做呢。
首先,完善的注释文档是必不可少的,对于每一个外界能够直接调用的方法,都应该说明该方法的具体实现。但是这么做却会将该方法的具体实现暴露在外,破坏了封装性。
其次,应该合理的提供访问修饰符为potected的钩子(Hook)方法,这么做可以减少子类的维护成本。例如AbstractList类中的removeRange方法,AbstractList类中的clear方法与内部类SubList类中的removeRange方法的实现都是基于这个方法的,这样在子类中只需要对AbstractList类中的removeRange方法进行修改即可。
还有一点需要注意的是,构造器绝不可以调用可以被覆盖的方法,因为超类构造器在子类之前运行,所以被子类覆盖的构造方法有可能会调用到未被初始化的变量,例如:
public class Super{
public Super() {
overrideMe();
}
public void overrideMe(){}
}
public final class Sub extends Super {
private final Date date;
Sub(){
date = new Date();
}
@Override
public void overrideMe() {
System.out.println(date);
}
public static void main(String[] args) {
Sub sub = new Sub();
}
}
运行上面这段代码,预期输出是当前的时间,但是实际上输出的将会是null,这是因为在被覆盖的overrideMe方法被父类构造器调用时,Date还没有被初始化。