本周重点是Liskov可替换原则。它要求父类和子类的行为一致性,子类要有更强的不变量、更弱的前置条件、更强的后置条件。在该原则的要求下,每个子类都可以对父类进行替换。这在开发过程中会带来极大的便利,在实验3中学习并运用该原则。
有关复用
1.复用的级别
-源代码级别的复用
-模块级别的复用(类/抽象类/接口)
-库级别的复用(API/包)
-系统级别的复用(框架)
2.软件构造过程中任何实体都可能被复用。比如需求、规约、数据、测试用例、文档等。
3.白盒复用对应继承,黑盒复用对应委托(显示委托:明确指出调用某个对象的功能;隐式委托:不明确指出调用某个对象的功能,例如敲击键盘)
子类型多态
客户端可用统一的方式处理不同类型的对象
协变和反协变
1.协变:子类型方法的返回值不变或者更加具体(原返回值的子类)、子类型抛出的异常不变或者更加具体(原异常类型的子类)
2.反协变:子类型方法的参数值不变或者更加抽象(原类型的父类),或者不抛出
但是对Java而言不支持,因为被视为overload
3.数组是协变的。Java不支持泛型数组,因为运行时存在泛型擦除。
Liskov可替换原则
(要求父类和子类的行为一致性)
-更强的不变量
-更弱的前置条件
-更强的后置条件
Java编译器有关继承的规则
-子类型可以增加方法,但是不能删除方法
-子类型需要实现抽象类型中的所有未实现方法
-子类型中重写的方法的 返回值 必须 相同/子类型/符合协变
-子类型中重写的方法的 参数 必须 相同/符合协变
-子类型中重写的方法不能抛出额外异常
A是父类,B是子类,A中有方法someMethod(Integer n),B中有方法someMethod(Number n)。如果A a = new B(),并且向someMethod传入Integer、Number,则分别调用哪个类的方法?
答:传入Integer类型调用A的方法,传入Number类型报错。
泛型中的LSP
1.ArrayList是List的子类型
List不是List的子类型
如果两个泛型存在父子类型关系,则泛型的类型必须相同。因为运行时存在泛型擦除。编译阶段报错,因为运行时进行擦除后相同
2.通配符可以解决泛型擦除问题,比如List<?>(说明任何类型都可用),用于和泛型的具体类型无关的场合,或者类型使用了Object类的操作,即放入什么类型都不影响使用。
3.<? super A>表示可存A和A的父类,<? extends A>表示可存A和A的子类。
例如,List是List<?>的子类型,同样是List<? extends Object>的子类型。List是List<? super String>的子类型。
委托和组合
1.建立委托之间的联系。
2.相较于继承,委托是比较细粒度的复用方式。一般来说,只需要用到一个类的部分而非全部方法时,使用委托。当一个对象需要“存在一个”或者“使用一个”,而不是“是一个”时使用委托。
3.不需要两个类之间存在语义联系。在实验2中,FriendshipGraph和Graph存在明显的父子类关系,可用继承。也可用委托。
4.委托发生在对象层面,继承发生在类的层面。
5.在计算奖金的例子中,核心问题是每个Employee对象的奖金计算方法都不同,在对象层面而非类层面。
6.动物例子。考虑动物的飞和叫。
将飞法设置一个接口,针对不同的飞法,实现该接口的多个类。叫法同理。然后将两个接口整合到鸭子的接口(用extends),再实现鸭子的类。最后继承该类实现种类细分。
具体实现(右下角的Duck最好修改为Ducklike,因为声明变量最好用父类)
7.委托的分类
委托分为使用(临时性)和关联(永久性)。
以下例子是临时性委托,只有执行方法时才产生关联
以下是永久性委托,将使用的类内化成属性,属于对象的一部分。又分为组合(调用类无法改变,更强的关联)和聚合(调用类可以改变,更弱的关联)
上为聚合,下为组合