Java:Effective java学习笔记之 复合优先于继承

复合优先于继承

面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。同样地,在《阿里巴巴Java开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现。

1、实现继承和接口继承

实现继承:当一个类扩展另一个类的时候
接口继承:当一个类实现一个接口的时候,或者当一个接口扩展另一个接口的时候。

本篇的继承说的是实现继承。

2、在实际开发中继承的缺点

与方法调用不同,继承打破了封装性:

  • 子类依赖于其超类中的特定功能的实现细节,
  • 如果需要覆盖超类的方法,就必须要知道超类所有的方法的内部逻辑,否则会照成意想不到的事故 举个例子:

父类有a,b,c三个方法,而子类覆盖了b,c两个方法,b里面有super.a,当外部调用子类的的b方法,如果a有c方法的调用,而且你不知道a方法调用了c方法,而且你还修改了c方法的逻辑,那可能会造成意想不到的结果

  • 1、超类的实现会随着发行版本的不同而内部逻辑可能会有变化
  • 2、如果要继承超类,超类必须要有详细的说明文档,否则增加学习成本
  • 3、暴露实现细节,可能导致客户端直接访问这些内部细节
  • 4、限制在原始的实现上,永远限定了类的性能
  • 5、导致语义上的细节
  • 6、可能客户直接修改超类,从而破坏子类的约束条件

以上缺点不适合的范围:首先项目中超类,父类,子类完全由你一个人完成,并且不会有其他人插手,别人也不会扩展你的超类,基类,也不会使用你完成的子类。

2.1、子类依赖于其超类中特定功能的实现细节

与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能随着发行版本的不同而变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有修改过。因而,子类必须要跟着其超类的更新而演变。

书上的例子:

/**
 * @description: 复合优先于继承
 **/
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<>();
        s.addAll(Arrays.asList("A","B","C"));
        System.out.println("AddCount = "+ s.getAddCount()); // 这个输出 AddCount = 6
    }
}

这个题目应该我们加了三个数字进去,为什么输出是6呢?
在这里插入图片描述

因为HashSet的内部addAll实现依赖于add。所以这个类再调addAlladdCount += c.size()这条语句先加了3,然后调super.addAll时,addAll调add,add又被子类实现了,addCount++,这句话每次增加一个就加1.

我们去掉覆盖的addAll方法,这个子类就可以修正了,但是这个功能的正确性依赖于HastSet的addAll方法是在add方法上实现的,万一后面变了,我们这个功能就不正确了。还有一种方法,覆盖addAll方法,遍历指定数组调用add方法,相当于重新实现了超类的方法,有时候这种做法不是一直可以的,因为无法访问对于子类来说的私有域,有些方法就无法实现。

3、什么是复合?

有一种方法可以避免以上的所有问题。不用扩展现有的类,而是在新的类中增加一个私有对象,它引用现有类的一个实例,这种设计被称作为复合,因为现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这称为转发,新类中的方法被称为转发方法

复合也可为称为装饰者模式,那么如何使用装饰者模式呢。

  • 1、创建被装饰接口
  • 2、创建被装饰类
  • 3、创建装饰类

我们就以装饰水果篮子为例。

  • 装饰类就是:Apple、Orange等水果
  • 被装饰类就是:Basket

1、首先创建被装饰接口(Basket):

public interface Basket {
	//该方法展示篮子里面有什么
	void show();
}

2、创建被装饰类(直接继承接口,重写方法)

public class EmptyBasket implements Basket {
	@Override
	public void show() {
		System.out.println("I am a basket");
	}

3、创建装饰类以Apple为例

public class AppleBasket implements Basket {
	//接收上一个装饰类
	private Basket basket;
	public AppleBasket(Basket basket) {
		this.basket = basket;
	}
	@Override
	public void show() {
		//调用上一个装饰类的方法
		basket.show();
		//本身的装饰
		System.out.println("I am apple");
	}
}

使用:

Basket basket = new AppleBasket(new OrangeBasket(new EmptyBasket()));
basket.show()

3.1、书上案例

请看下面的例子,他用复合/转发的方法来代替InstrumentedHashSet类。

注意这个实现分为两部分:类本身和可重用的转发类(forwarding class),包含了所有的转发方法,没有其他方法。

/**
 * @description: 复合
 **/
public 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.removeAll(c);
    }

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

新增了一个类-转发类(FowardSet),内部增加了私有域,并引用实例s(复合),然后再让包装类,也就是继承中的子类来继承使用。

/*
 * 包装类(wrapper class),采用装饰者模式
 */
public class InstrumentedHashSet<E> extends ForWardingSet<E>  {
    private int addCount = 0;

    public InstrumentedHashSet(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;
    }
}

因为每一个InstrumentedHashSet实例都把另一个Set实例包装起来了,所以InstrumentedHashSet类被称为包装类。这也正是Decorator(装饰)模式,因为InstrumentedSet类对一个集合进行了装饰,为它增加了计数特性。

简单来讲就是将继承的子类作为新类实例中一个组成部分来重写。(类似子类相当于发动机,转发类相当于车子,子类变成其中一部分

4、复合相比较于继承的优点和缺点

1、优点

  • 通过在新类增加一个私有域,引用原本的超类(后面同意叫需要叫现有类),使现有类变成新类的一个组件,而新类的方法都可以调用现有类里面的对应的方法,这个也叫转发
  • 没有打破封装,就算现有类添加新的方法,或者修改原来方法的逻辑(方法入参和返回结果不能有改变),也不会影响到新类,对于封装的优点自行查询

复合和转发的结合也被宽松的称为"委托"

2、缺点:

  • 不适合回调框架,因为回调框架是把对象自身的引用传递给其他的对象,用于后续的调用,但是包装起来的对象并不知道它外面的对象,所以它传递一个执行自身的引用,回调时避开了外面的包装对象,这也被称为SELF问题

5、何时使用继承,何时使用复合?

1、只有当子类真正是超类的子类型时,也就是说,对于两个类A和B,只有当两者之间确实存在"is-a"关系的时候才使用继承,否则使用复合。

2、如果子类只需要实现超类的部分行为,则考虑使用复合。

3、如果你试图扩展的类它的API有缺陷,继承机制会把缺陷传递给子类,而复合则允许设计新的API来隐藏这些缺陷。

注意:

  • 1、超类的构造器不要调用子类可覆盖的方法(直接间接都不行)
  • 2、clone和readObject不可调用可覆盖的方法(直接间接都不行)
  • 3、对于不想让子类覆盖的方法,就要禁止此方法子类化
    • 声明类为final
    • 将构造器都变成私有的,并增加公有的静态工厂代替构造器。

总结:

简而言之,继承的功能非常强大,但也存在诸多问题,因为违背了封装原则。只有当子类和超类确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性,为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能更加强大。

参考

1、为什么讲复合优先于继承
2、第十六条:复合优先于继承
3、第十六条:复合优先于继承
4、为什么组合优先于继承

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值