类和接口-复合优先于继承

258 阅读3分钟

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

前言

继承要考虑是否使用的适当,当我们在很小的一个范围或者一个程序员的控制下使用继承的时候,使用继承很安全,但如果我们使用了其他人的api,并对其中的一个类进行了实现继承(不是对接口进行继承),那此时就将会十分的危险,如果下次对方对此类进行了一些修改,那么将有可能造成意料之外的情况发生。

一个例子

public class TestHashSet<E> extends HashSet<E> {
    private int count = 0;

    public TestHashSet() {        
    }
    public TestHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        count++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        count += c.size();
        return super.addAll(c);
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        TestHashSet<String> hashSet = new TestHashSet<String>();
        hashSet.addAll(Arrays.asList(new String[]{"1","2","3"}));
        System.out.println(hashSet.getCount());
    }
}

我们期望它能返回3,但是实际上返回的是6 image.png 这个类不能正常工作的原因是因为在super的addAll方法中,实际上使用了add方法,而这里的add方法又被我们 的TestHashSet这个类覆盖了,就导致addAll中加了3,每次调用add又加了1,调用了3次,就是6了。

如何解决

复合的方式来解决继承导致的这种依赖于现有类的实现细节的问题,复合不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。 因为现有类变成了一个新类的一个组件,新类中的每个实例方法都能调用被包含的类的实例方法,并返回相应的结果,这称之为转发。 对应的实现分为两部分,类本身和可重用的转发类。

类本身

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实现了。

可重用的转发类

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

转发类这里我们可以看到它的构造方法中将得到的Set类型赋给了它内部的私有域s,从而使得我们可以使用被包含的现有类实例中对应的方法。

包装类的缺点

包装类不适用于回调框架,回调框架中对象会将自身的引用传递给其他的对象,但因为被包装的对象并不知道它外面的包装对象,此时回调会避开外面的包裹类