【笔记18】组合优于继承

继承是代码重用手段,但会打破封装性,子类依赖超类实现细节,超类变化可能破坏子类。文中以继承扩展HashSet为例说明问题,结果与预期不符。解决办法是使用组合和转发,实现新类和转发类,包装类更健壮、功能更强,仅在有父子关系时才考虑继承。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承(inheritance)是实现代码重用的有力手段,但并非总是最好的选择。【本节“继承”指一个类扩展另一个类时的实现继承】

与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而有所变化,一旦发生变化可能导致子类遭到破坏。

反例【子类遭到破坏】

有一个程序使用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;
    }
    
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
        s.addAll(Arrays.asList("s","a","p"));
        System.out.println(s.getAddCount());
    }
}

我们看main方法,考虑上面的提供计数功能的HashSet。我们希望得到的结果是3,但结果却是6。

why??

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

只要去掉覆盖的addAll方法就可以修正问题,虽然这个类可以正常工作了,但是HashSet的addAll方法是实现细节,不是承诺,意味着不能保证addAll的实现方法在未来发行的版本保持不变。

因此,使用继承扩展一个类很危险,父类的具体实现很容易影响子类的正确性。

 

解决:使用组合和转发

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

//Reusable forwarding class
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.retainAll(c);}

    @Override
    public void clear() {s.clear();}
    
}
//Wrapper class - use composition in place of inheritance
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;
    }
}

Set接口的存在使得InstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。.无论是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));

只有当子类和超类之间确实存在父子关系时,才可以考虑使用继承。否则都应该用组合,包装类不仅比子类更加健壮,而且功能也更加强大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值