effective java 之复合优先于继承
1、 继承是实现代码重用的重要手段,但它并非永远是完成这项工作的最佳工具,不恰当的使用会导致程序变得很脆弱。
在包的内部使用继承是非常安全的,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。
然而们对于进行跨越包边界的继承,则要非常小心。“继承”在这里特指一个类扩展另一个类。
继承打破了封装性,因为子类依赖于超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而有所变化,导致子类遭到破坏。
2、 为什么复合优先于继承
1.继承违反了封装原则,打破了封装性
2.继承会不必要的暴露API细节,称为隐患.比如通过直接访问底层使p.getProperty(K,V)的键值对可以不是String类型
3.继承限定了类的性能,它会把它的缺陷传递给子类
1.复合:不必扩展现有的Set类,而是在此类中加一个私有域,它引用现有类的一个实例
2.它的封装特性使得它更加健壮灵活
3.复合允许设计新的API隐藏父类的缺点
3、具有计数功能的HashSet
public class InstrumentedHashSet<E> extends HashSet<E> {
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());
}
}
我们希望得到的结果是3,但结果却是6,原因是addAll方法是基于add方法实现的,InstrumentedHashSet的addAll方法首先将addCount加3,然后super.addAll调用HashSet的addAll方法,而addAll又分别调用到InstrumentedHashSet覆盖了的add方法,每个元素调用一次,又给addCount加了3,结果是6。
只要去掉覆盖的addAll方法就可以修正问题,虽然这个类可以正常工作了,但是HashSet的addAll方法是实现细节,不是承诺,意味着不能保证addAll的实现方法在未来发行的版本保持不变。
稍微好一点的方法是覆盖addAll方法来遍历指定的集合,为每个元素调用一次add方法,这样可以保证得到正确的结果,然而这种方法对于要访问超类的私有域就无能为力了。
覆盖超类的每一个添加元素的方法,如果超类以后增加一种新的添加元素方法,那就可能有“非法的元素”添加到集合中。
不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例,可以避免前面提到的问题,这种设计称为“复合”,因为现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。
3、使用复合和转发
使用复合来扩展一个类需要实现两部分:新的类和可重用的转发类。转发类用于将所有方法调用转发给私有域。这样得到的类非常稳固,不依赖于现有类的实现细节。
/* 新类
* 转发:新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法并返回结果
*/
public class ForwardingSet<E> implements Set<E> {
private Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
public boolean contain(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 retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public boolean equals(Object o) {
return s.equals(o);
}
public int hashCode() {
return s.hashCode();
}
public String toString() {
return s.toString();
}
@Override
public boolean contains(Object o) {
// TODO Auto-generated method stub
return false;
}
@Override
public <T> T[] toArray(T[] a) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
}
//复合:不必扩展现有的Set类,而是在此类中加一个私有域,它引用现有类的一个实例
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
//引用ForwardingSet实例
public InstrumentedSet(Set<E> s) {
super(s);
}
public boolean add(E e) {
addCount++;
return super.add(e);
}
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());
}
}
打印结果:3
现在使用InstrumentedSet不会再出上面的问题了,因为无论是add方法还是addAll方法都转发给了私有域s来处理,这些方法对于s来说总是一致的,不会受InstrumentedSet的影响。
另一个好处是此时的包装类InstrumentedSet可以用来包装任何Set实现,有了更广泛的适用性。
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
ForwardingSet作为一个转发类,拥有一个Set实例变量,而InstrumentedSet把add和addAll方法覆盖掉,实现计数功能。
4、只有当子类和超类之间确实存在父子关系时,才可以考虑使用继承。否则都应该用复合,包装类不仅比子类更加健壮,而且功能也更加强大。
转发(forwarding):新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。新类中的方法被称为“转发方法”。
每天努力一点,每天都在进步。