王大虎:增加AK47和AUG后的Rifle子类图

2018年4月4日

王大虎

很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击X,源代码如代码清单2-7所示。

代码清单2-7 AUGX源码代码

public class AUG extends Rifle { //X都携带一个精准的望远镜 public void zoomOut(){ System.out.println(“通过望远镜察看敌人…”); } public void shoot(){ System.out.println(“AUG射击…”); } }

有X就有狙击手,狙击手类的源代码如代码清单2-8所示。

代码清单2-8 AUG狙击手类的源码代码

public class Snipper { public void killEnemy(AUG aug){ //首先看看敌人的情况,别杀死敌人,自己也被人干掉 aug.zoomOut(); //开始射击 aug.shoot(); } }

狙击手,为什么叫Snipper?Snipe翻译过来就是鹬,就是“鹬蚌相争,渔人得利”中的那只鸟,英国X到印度打猎,发现这个鹬很聪明,人一靠近就飞走了,没办法就开始伪装、远程精准射击,于是乎Snipper就诞生了。

狙击手使用X来杀死敌人,业务场景Client类的源代码如代码清单2-9所示。

代码清单2-9 狙击手使用AUG杀死敌人

public class Client { public static void main(String[] args) { //产生三毛这个狙击手 Snipper sanMao = new Snipper(); sanMao.setRifle(new AUG()); sanMao.killEnemy(); } }

狙击手使用G3杀死敌人,运行结果如下所示:

王大虎

通过望远镜察看敌人… AUG射击…

在这里,系统直接调用了子类,狙击手是很依赖X的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递了进来。这个时候,我们能不能直接使用父类传递进来呢?修改一下Client类,如代码清单2-10所示。

代码清单2-10 使用父类作为参数

public class Client { public static void main(String[] args) { //产生三毛这个狙击手 Snipper sanMao = new Snipper(); sanMao.setRifle((AUG)(new Rifle())); sanMao.killEnemy(); } }

 

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

3.覆盖或实现父类的方法时输入参数可以被放大

方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。这个比较难理解,我们来看一个例子,我们先定义一个Father类,如代码清单2-11所示。

代码清单2-11 Father类源代码

public class Father { public Collection doSomething(HashMap map){ System.out.println(“父类被执行…”); return map.values(); } }

这个类非常简单,就是把HashMap转换为Collection集合类型,然后再定义一个子类,源代码如代码清单2-12所示。

代码清单2-12 子类源代码

public class Son extends Father { //放大输入参数类型 public Collection doSomething(Map map){ System.out.println(“子类被执行…”); return map.values(); } }

请注意粗体部分,与父类的方法名相同,但又不是覆写(Override)父类的方法。你加个@Override试试看,会报错的,为什么呢?方法名虽然相同,但方法的输入参数不同,就不是覆写,那这是什么呢?是重载(Overload)!不用大惊小怪的,不在一个类就不能是重载了?继承是什么意思,子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不相同,当然是重载了。父类和子类都已经声明了,场景类的调用如代码清单2-13所示。

代码清单2-13 场景类源代码

public class Client { public static void invoker(){ //父类存在的地方,子类就应该能够存在 Father f = new Father(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); } }

代码运行后的结果如下所示:

 

父类被执行…

王大虎

根据里氏替换原则,父类出现的地方子类就可以出现,我们把上面的粗体部分修改为子类,如代码清单2-14所示。

代码清单2-14 子类替换父类后的源代码

public class Client { public static void invoker(){ //父类存在的地方,子类就应该能够存在 Son f =new Son(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); } }

运行结果还是一样,看明白是怎么回事了吗?父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较理难理解,我们再反过来想一下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。我们把上面的例子修改一下,扩大父类的前置条件,源代码如代码清单2-15所示。

代码清单2-15 父类的前置条件较大

public class Father { public Collection doSomething(Map map){ System.out.println(“Map 转Collection被执行”); return map.values(); } }

把父类的前置条件修改为Map类型,我们再修改一下子类方法的输入参数,相对父类缩小输入参数的类型范围,也就是缩小前置条件,源代码如代码清单2-16所示。

代码清单2-16 子类的前置条件较小

public class Son extends Father { //缩小输入参数范围 public Collection doSomething(HashMap map){ System.out.println(“HashMap转Collection被执行…”); return map.values(); } }

在父类的前置条件大于子类的前置条件的情况下,业务场景的源代码如代码清单2-17所示。

王大虎

代码清单2-17 子类的前置条件较小

 

public class Client { public static void invoker(){ //有父类的地方就有子类 Father f= new Father(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); } }

代码运行结果如下所示:

没有评论

评论已关闭。