《Effective Java》阅读笔记16 复合优先于继承

853 阅读4分钟

1.继承的缺点

  • 继承不容易控制,使用不当容易导致软件非常脆弱,特别是继承不再同一个包下的类
  • 继承打破了父类的封装性,有的时候父类的内部实现改变,可能会导致子类遭到破坏。

举个例子: 假设有一个程序使用HashSet,为了查看它自创建以来曾经添加过多少个元素,我们可以通过继承扩展HashSet,重写add和addAll方法。


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;
  }
}

这段代码看上去没什么问题,假如执行下面的程序,我们期望getAddCount返回3,但它实际上返回的是6。

InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());

哪里出错了?

在HashSet内部,addAll方法是基于add方法来实现的,即使HashSet的文档中并没有说明这一细节,这也是合理的。因此InstrumentedHashSet中的addAll方法首先把addCount增加了3,然后利用super.addAll()调用HashSet的addAll实现,在该实现中又调用了被InstrumentedHashSet覆盖了的add方法,每个元素调用一次,这三次又分别给addCount增加了1,所以总共增加了6。

==因此,使用继承扩展一个类很危险,父类的具体实现很容易影响子类的正确性。而复合优先于继承告诉我们,不用扩展现有的类,而是在新类中增加一个私有域,让它引用现有类的一个实例。这种设计称为复合(Composition)。==

2.什么是复合

复合就是在你的类中添加一个私有域,引用一个类的实例,使被引用类成为引用类的一个组件。

什么是转发 转发(fowarding):新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回结果。

3.使用复合的原则

  • 为想要被继承的类设计一个转发类
  • 继承者转发类
  • 覆盖想要覆盖的方法,或者添加想要添加的方法。

4.使用复合实例

使用复合来扩展一个类需要实现两部分:==新的类====可重用的转发类==。转发类用于将所有方法调用转发给私有域。这样得到的类非常稳固,不依赖于现有类的实现细节。请看下面的例子。

//Wrapper class - use composition in place of inheritancepublic 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;
    }}
//Reusable forwarding classclass 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.retainAll(c);}

    @Override
    public void clear() {s.clear();}
    }

现在,使用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));

5.复合的缺点

包装类不适合用在回调框架中,在回调框架中对象把自身的引用传递给了其它的对象,用于后续的调用,因为被包装起来的对象并不知道外面的包装对象,所以它传递一个指向自身的引用(this),回调避开了外面的包装对象。这被称作SELF问题

6.总结

只有当子类和超类之间确实存在父子关系时,即便如此,==如果子类和超类存在不同的包中,并且超类并不是为继承而设计的,那么继承将导致脆弱性,可以用复合和转转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大==。

7.参考文献

www.jianshu.com/p/4fd034505…

《Effective Java》

www.cnblogs.com/jjfan0327/p…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享