• Java


    继承是实现代码重用的方法之一,但使用不当则会导致诸多问题。


    继承会破坏封装性,对一个具体类进行跨包访问级别的继承很危险。
    即,子类依赖父类的实现细节。
    如果父类的实现细节发生变化,子类则可能遭到破坏。


    举个例子,扩展HashSet,记录HashSet实例创建以来一共进行了多少次添加元素的操作。
    HashSet有两个添加元素的方法——add(E e)和addAll(Collection<? extends E> c)。
    那就覆盖这两个方法,在添加操作执行前记录次数:

    public class InstrumentedHashSet<E> extends HashSet<E> {
        // The number of attempted element insertions
        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;
        }
    }
    


    测试一下,通过addAll方法添加3个元素:

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
        System.out.println(s.getAddCount());
    }
    

    结果是: wKioL1PLrWjBf_fsAADIJ3WV64w978.jpg


    导致这种结果的原因很简单。
    参考AbstractCollection中的add(E e)和addAll(Collection<? extends E> c), add(E e)中只有一段throw new UnsupportedOperationException();
    而addAll(Collection<? extends E> c)的文档注释中有这么一段话:

    * <p>Note that this implementation will throw an
    * <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is
    * overridden (assuming the specified collection is non-empty).
    

    解决这个问题的方法很简单,只需要去掉覆盖的addAll方法即可。
    但这样却不能解决根本问题,即HashSet的addAll方法不保证在以后的发行版本中不发生变化。
    即,子类实现依赖父类实现,父类发生变化时子类遭到破坏。
    也许我们可以覆盖父类方法重新实现,虽然解决问题,但这样费力不讨好,毫无意义。


    另外,父类增加或者移除方法也会对子类产生影响。
    举个例子,子类扩展了某个集合类,覆盖了所有添加元素的方法,在添加元素之前对元素进行检查,让所有元素满足某个条件。
    如果在后来的版本中,父类增加了新的添加元素的方法,而子类没有覆盖该方法,导致非法元素添加到集合中。


    反之,也有可能出现这种情况。
    即便父类的实现没有问题,但也可以因为子类实现不当而破坏父类的约束。
    比如,父类恰好增加了和子类相同签名和返回类型的方法。


    于是,为了应对这些情况,可以使用复合模式(composition)代替继承。
    即,在一个forwarding class中增加一个private field引用现有类的实例,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();
        }
    }
    


    使用时直接继承forwarding class:

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


    forwarding class通过Set接口提供了相应方法。
    这种设计也有其灵活性,继承只能选择Set的某个特定实现,但使用复合我们可以选择任何接口实现。
    比如:

    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<String>(
                new HashSet<String>());
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
        System.out.println(s.getAddCount());
    }
    


    如何从继承和复合之间做出选择?
    比较抽象的说法是,只有子类和父类确实存在"is-a"关系的时候使用继承,否则使用复合。
    或者比较实际点的说法是,如果TypeB只需要TypeA的部分行为,则考虑使用复合。

  • 相关阅读:
    linux常用命令
    10.8统计英文词频
    9月10号作业
    华氏温度与摄氏温度转换
    小故事
    Java的工厂模式(三)
    Javascript实现图片翻转
    Java的工厂模式(二)
    Java的工厂模式(一)
    Java新建线程的两种方式
  • 原文地址:https://www.cnblogs.com/kavlez/p/4244865.html
Copyright © 2020-2023  润新知