TIP13.使类和成员的可访问性最小化(封装)
- 实例域决不能是公有的
- 不应当具有公有的静态final数组域,或者返回这种域的方法。作为替代,你可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] VALUES[] = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
或者,添加一个公有方法来返回该数组的一个备份
public static final Thing[] values(){
return VALUES.clone();
}
TIP14.在共有类中使用访问方法而非公有域
- 共有类永远不应该暴露可变的域
- 如果类是包私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质错误
TIP15.使可变性最小化(不可变类)
- 不可变类,即实例不能被修改的类。所有信息必须在创建实例时提供,并在对象的整个生命周期内固定不变。
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都是私有的
- 确保对任何可变组件的互斥访问
使用functional的方式来获取新状态的对象
考虑不可变类Point拥有两个域x和y,需要得到新的Point(x+deltaX,y+deltaY),则应该这样处理:
pulibc Point add(float deltaX,float deltaY){
return new Point(this.x+deltaX,this.y+deltaY);
}
不可变类的一些特性
- 它是线程安全的,因此可以被自由的共享。
频繁重用:对一些经常用到的值,提供公有的static final常量,比如:
public static final Point PointZero = new Point(0.0,0.0)
不需要,也不应该为不可变类提供clone方法,或拷贝构造器。
- 不可变对象为其他对象提供了大量的构件(building blocks)。
- 唯一的缺点是,对于每个不同的值,都需要一个单独的对象,可能导致性能问题。
防止被扩展
- 使用final修饰不可变类
- 构造器私有,或包私有,然后用静态工厂方法来代替构造器
无法使用不可变类
有些类不能做出不可变的,仍然应该尽可能的限制他的可变性。
除非有足够的理由使域变成非final的,否则,每个域都应该是final。
不要在构造器和静态工厂之外,再额外的提供初始化方法,除非有令人信服的理由必须这么做。
TIP16.复合优先于继承
继承可能出现的问题
继承打破了封装性,子类依赖于父类中特定功能的实现细节。
考虑下面的InstrumentedHashSet类,扩展了HashSet,用addCount来记录已添加的元素数量。
import java.util.*;
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
s中的确只添加了3个元素,但s.getAddCount()返回的值是6。
期望的调用过程是这样的:
s.addAll(e1,e2,e3) ->
[
addCount+=3;
s.super.addAll(e1,e2,e3) ->
[
s.super.add(e);
] * loop(3)
]
但因为子类覆盖了父类的add(e)方法,由于多态机制,会调用子类的实现。
于是实际的调用过程是这样的:
s.addAll(e1,e2,e3,....) ->
[
addCount+=3;
[
s.add(e1,e2,e3) ->
[
addCount ++;
s.super.add(e);
]
] * loop(3)
]
如果子类不覆盖addAll方法,就可以修正这个类。
然而,这样的类虽然可以正常工作,但它的功能正确性却依赖于:父类的addAll是在它的add方法上实现的。
这种“自用性”是实现细节,不是承诺。父类不承诺这个实现永远不变,而一旦发生改变,子类仍然可能无法正常工作。
如果子类覆盖addAll方法来遍历参数集合,为每个元素调用一次(已覆盖的)add方法,可以得到正确的结果,不管父类的addAll是否依赖add实现。因为父类的addAll实现将不会再被调用到。这实际上相当于重新实现了超类的方法。而这样做并不可靠,因为无法访问父类的私有域,可能有些方法无法实现。
使用复合(composition)
转发(forwarding): 新类中的每个实例方法,都可以调用被包含的现有实例中对应的方法,并返回它的结果。这个过程称为转发。
下面重新实现上面的例子:
import java.util.*;
//可重用的转发类
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
import java.util.*;
//实现类
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s =
new InstrumentedSet<String>(new HashSet<String>());
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
这正是装饰模式(Decorator)。
is-a还是has-a问题
如果不确定,一律考虑用复合的方式。
Java平台类库中,有一些违反这条原则的地方。比如stack并不是vector,所以Stack不应该扩展Vector。