这是很多设计模式的初衷,设计模式的七大原则之一就是复合原则,继承虽然可以实现代码重用,但是会增加耦合度,打破了封装性。子类依赖于超类中的某些细节,如果超类随着版本升级内部发生了改变,那么所有的子类都要随之改变。
为了具体说明,假设有一个使用HashSet
的程序。 为了调整程序的性能,需要查询HashSe
,从创建它之后已经添加了多少个元素(不要和当前的元素数量混淆,当元素被删除时数量也会下降)。 为了提供这个功能,编写了一个HashSet
变体,它保留了尝试元素插入的数量,并导出了这个插入数量的一个访问方法。 HashSet
类包含两个添加元素的方法,分别是add
和addAll
,所以我们重写这两个方法:
package item18;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author: qujundong
* @date: 2020/11/27 下午9:57
* @description:
*/
public class InstrumenteHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumenteHashSet(){}
public InstrumenteHashSet(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) {
InstrumenteHashSet<String> s = new InstrumenteHashSet<>();
String[] array = {"Snap", "Crackle", "Pop"};
s.addAll(Arrays.asList(array));
System.out.println(s.getAddCount());
}
}
上述代码我们期望输出3,但是输出6,以为HashSet中addAll内部是调用的add,但是这样的细节,子类继承后可能会忽略,所以导致出错。
导致子类脆弱的原因:
- 超类发生改变会导致子类出现问题
如果子类继承后并覆盖了所有超类的方法,但是当超类增加了新方法,而子类中并没有对该类覆盖时,则会出现问题,比如下例子,usePrint是超类新加的方法,本来要用超类的print,但是由于覆盖,使用了子类的。
/** * @author: qujundong * @date: 2020/11/27 下午10:11 * @description: */ public class Person { public void print(){ System.out.println("this is person"); } public void usePrint(){ System.out.println("this is usePrint, want to use person print "); print(); } } public class Man extends Person { public void print(){ System.out.println("this is man"); } public static void main(String[] args) { Person man = new Man(); man.usePrint(); } } /* this is usePrint, want to use person print this is man */
如果子类不覆盖超类方法,只是新加一些方法,开始是不会出现问题,但是当超类增加了和子类相同名字的方法时又回到了1, 2问题。
可以使用组合来修改上述问题:
package item18;
/**
* @author: qujundong
* @date: 2020/11/27 下午10:22
* @description:
*/
// Reusable forwarding class
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
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();
}
}
package item18;
/**
* @author: qujundong
* @date: 2020/11/27 下午10:23
* @description:
*/
// Wrapper class - uses composition in place of inheritance
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class ComInstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public ComInstrumentedSet(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) {
TreeSet<String> set = new TreeSet<>();
set.add("123");
set.add("abc");
set.add("bcd");
ComInstrumentedSet<String> instrumenteHashSet = new ComInstrumentedSet<String>(new TreeSet<>());
instrumenteHashSet.addAll(set);
System.out.println(instrumenteHashSet.getAddCount());
}
}
总结:总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。