第十六条 复合优先于继承

封装、继承、多态是java的三大特征,而封装和继承有点对立。封装是指把一个功能封装到方法里,隐蔽细节,直接暴露给他人调用;继承则是代码重用的有效手段,把共同的代码抽取到一个基类里,子类去继承,这样能减轻工作量和维护量,但如果使用不当,程序也会变得很脆弱。好的写法可以通过继承来建立一套从上到下的骨架类型的类,差劲的就很明显的打破了封装性,子类会及其依赖于父类的功能,一旦修改稍有不慎,很容易出问题。本章有个例子,现在对例子进行解析

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

    InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.add("1");
        s.add("2");
        s.add("3");

    int size = s.getAddCount(); 此时s的值为3,正确;但是,如果调用另外一个方法

    InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(Arrays.asList("a", "b", "c"));

        int size = s.getAddCount(); 此时s的值为6,不正确;命名穿进去三个值,为什么变成6个呢? 我们点进去addAll()方法,super()源码为    
    
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }


里面回去调用add(e)方法,InstrumentedHashSet重写了add()方法,父类的里的方法互相调用时,如果子类重写了,会执行子类中的方法,我们子类的方法是addCount++;return super.add(e); 计数的num增加了三次,然后调用父类的add()方法,所有3被算了两次,集合元素的计数为6。我们可以重写addAll()方法,不在这里面调用super方法,自己去实现逻辑,但这样做会很麻烦,并且集合个数的正确性要依赖于HashSet的方法;如果我们把子类的add()方法中addCount++;呢,这样调用addAll()是没问题,但单独调用add()方法则不会计数;还有一个方法呢,把子类addAll()方法中的addCount +=c.size();去掉,但是这样的耦合依赖关系太严重了,如果某天,java把HashSet的源码做出修改,不依赖于add()方法,我们怎么办?上面问题之所以是问题,是因为一些源码不固定,会因为效率或其他原因而被修改,我们使用时不能完全掌握或需要消耗比较大的代价才能掌握其中的细节,然后根据细节去实现子类自己的逻辑。这样子类就依靠父类了,耦合度提高,容易出问题,没有做到自己管理自己,不论父类怎么变化,只要符合功能的规范,子类就一直能保持正确性。如果要做到这一步,仅仅靠继承,重写子类的方法,是不够的。所以,复合登场了!复合不用去扩展父类,而是类似于代理,在类中增加一个私有成员变量,通过转发来实现交互,不在乎类的细节,能完美的实现功能。


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

    public class ForwardingSet<E> implements Set<E> {
        private final Set<E> s;

        public ForwardingSet(Set<E> s) {
            this.s = s;
        }

        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 addAll(Collection<? extends E> c) {
            return s.addAll(c);
        }

       ...
    }

    Set<String> originSet = new HashSet<>();
    ForwardingSet<String> forwardingSet = new ForwardingSet<>(originSet);
    InstrumentedSet<String> set = new InstrumentedSet<>(forwardingSet);
    set.add("1");
    set.addAll(Arrays.asList("a", "b", "c"));

这个例子中,一个类包裹着另外一个。ForwardingSet类实现了Set<E> 接口,这样就能保证Set集合中的方法都被重写;不实现Set<E>接口也行,只要保证把Set的方法写全就可以了,但一般只要稍微
不留意,就会出错,implements Set<E> 直接实现接口,安全又省力,implements Set<E>的作用仅仅是这个,并非复合的私有域非得实现一个接口,不要理解错了,笔者刚开始就理解偏了。
InstrumentedSet继承ForwardingSet方法,需要用Set的哪个方法,就重写一下,添加个addCount计数就行了。我们调用 InstrumentedSet 的 add()方法,会计数加一,然后调用ForwardingSet的add()方法,注意,此时起作用的是s.add(e);方法,s是什么呢,就是一开始创建的originSet ,这个对象传进了ForwardingSet的构造里,同理,addAll方法也是一样,调用了s.addAll(c);此时,s.addAll(c)方法会执行父类的添加方法,调用add()方法,但这时候的add()方法是 originSet 的add(),非是InstrumentedSet的add(),因此不会让计数额外增加。这样,不论HashSet内部逻辑怎么变换,都对我们这个类没影响。这就是复合的好处。

复合也有缺陷,因为是要明确知道包装的是什么,所以对于回调框架,因为回调框架是把自身对象引用传递给其他对象使用,因此不确定回调出来的是什么,包装类就不好用了。继承功能很强大,复
合也很巧妙,如例子中的 Properties 和 Stack,使用复合效果会更好,大家可以自己写个例子,体会一下。


 

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/82633514