实现继承:当一个类扩展另一个类的时候 接口继承:当一个类实现一个接口的时候,或者当一个接口扩展另一个接口的时候。 本篇的继承说的是实现继承。
继承使用不当会导致软件变得很脆弱,在包的内部使用,以及使用专门为了继承而设计并有很好文档说明的类来说,使用继承是非常安全的。但是,对普通的具体类进行跨越包边界的继承,则是非常危险的。
导致子类脆弱的原因:
第一种:子类依赖于其超类中特定功能的实现细节。
与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有修改过。因而,子类必须要跟着其超类的更新而演变。
书上的例子:
/**
* @description: 复合优先于继承
* @author: lty
* @create: 2021-05-21 14:10
**/
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<>();
s.addAll(Arrays.asList("A","B","C"));
System.out.println("AddCount = "+ s.getAddCount()); // 这个输出 AddCount = 6
}
}
这个题目应该我们加了三个数字进去,为什么输出是6呢?
因为HashSet的内部addAll实现依赖于add。所以这个类再调addAll时addCount += c.size()这条语句先加了3,然后调super.addAll时,addAll调add,add又被子类实现了,addCount++,这句话每次增加一个就加1.
我们去掉覆盖的addAll方法,这个子类就可以修正了,但是这个功能的正确性依赖于HastSet的addAll方法是在add方法上实现的,万一后面变了,我们这个功能就不正确了。还有一种方法,覆盖addAll方法,遍历指定数组调用add方法,相当于重新实现了超类的方法,有时候这种做法不是一直可以的,因为无法访问对于子类来说的私有域,有些方法就无法实现。
第二种:它们的超类在后续的发行版本中可以获得新的方法。
例如:如果超类在后续发行版本中获得了一个新的方法与子类签名相同但返回类型不同的方法,那么子类将无法通过编译。
(这个点如果想更加了解建议看书)
有一种方法可以避免以上的所有问题。不用扩展现有的类,而是在新的类中增加一个私有对象,它引用现有类的一个实例,这种设计被称作为复合,因为现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这称为转发,新类中的方法被称为转发方法。
调整后的代码
/**
* @description: 复合
* @author: lty
* @create: 2021-05-21 15:06
**/
public class ForWardingSet<E> implements Set<E> {
private final Set<E> s;
public ForWardingSet(Set<E> s){
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
}
/**
* @description: 复合优先于继承
* @author: lty
* @create: 2021-05-21 14:10
**/
public class InstrumentedHashSet<E> extends ForWardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet(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;
}
}
因为每一个InstrumentedHashSet实例都把另一个Set实例包装起来了,所以InstrumentedHashSet类被称为包装类。这也正是Decorator(装饰)模式,因为InstrumentedSet类对一个集合进行了装饰,为它增加了计数特性。
注意:包装类不适合用在回调框架中,在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用,回调时避开了外面的包装对象。这被称为SELF问题。
何时使用继承,何时使用复合?
只有当子类真正是超类的子类型时,也就是说,对于两个类A和B,只有当两者之间确实存在"is-a"关系的时候才使用继承,否则使用复合。
如果子类只需要实现超类的部分行为,则考虑使用复合。
如果你试图扩展的类它的API有缺陷,继承机制会把缺陷传递给子类,而复合则允许设计新的API来隐藏这些缺陷。
总结:
简而言之,继承的功能非常强大,但也存在诸多问题,因为违背了封装原则。只有当子类和超类确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性,为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能更加强大。