里氏替换原则是针对继承的,它为良好的继承定义了一个规范。
从学习面向对象编程开始,就知道了继承的概念,是面向对象的三大特征之一,但是继承究竟有什么好的呢?凡事都有两面性,在尽享继承带来的好处的同时,它又给我们带来什么弊端呢?这里我们来回顾一下:
好处:
1、代码共享,减少创建类的工作量,每个子类都拥有父类的方法和树形;
2、提高代码的重用性;
3、提高代码的可扩展性;
4、提高项目或产品的开放性;
缺点:
1、继承是侵入式的,只要继承就必须拥有父类所有的树形和方法;
2、降低代码的灵活性,子类必须继承父类的树形和方法,让子类多了很多的约束;
3、增强了耦合性,当父类的变量、常量和方法被修改时,必须考虑到子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的接口——大量的代码需要重构。
里氏替换原则就是针对继承的缺点而定义的规范,它的简明的定义如下:
只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要知道是父类还是子类。但是,反过来就不可以了,有子类出现的地方,父类未必就能适应。
简单的定义包含了4层含义:
举个例子,CS游戏中的枪,如下类图:
代码如下:
public abstract class AbstractGun { //枪都有一个特征,就是射击 public abstract void shoot(); }
public class HandGun extends AbstractGun { @Override public void shoot() { System.out.println("手枪射击..."); } }
public class Rifle extends AbstractGun { @Override public void shoot() { System.out.println("步枪射击..."); } }
public class MachineGun extends AbstractGun { @Override public void shoot() { System.out.println("机枪射击..."); } }
/** * 在类中调用其他类时务必使用父类或是接口,如果不能使用父类或是接口,则说明类的设计已经违背了LSP原则 * @author suo */ public class Soldier { private AbstractGun gun; public void setGun(AbstractGun gun){ this.gun=gun; } public void killEnemy(){ System.out.println("士兵开始杀敌人..."); gun.shoot(); } }
public class Client { public static void main(String[] args) { Soldier soldier = new Soldier(); soldier.setGun(new Rifle()); soldier.killEnemy(); } }
运行结果如下:
士兵开始杀敌人...
步枪射击...
此时,如果有一个玩具手枪,不能用来射击,杀不死人,该怎么办?首先不能让玩具手枪继承自AbstractGun,因为它没有射击的特征,可以这么来做,让玩具手枪脱离继承,建立一个独立的父类,为了实现代码的重用,可以与AbstractGun建立关联委托关系,如下类图:
如果子类不能完整实现父类的方法,或者是父类的某些方法在子类中已经发生“畸变”,那么建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
2、子类可以有自己的个性
之所以有继承,是为了在父类的继承上增加自己的特性,最大程度的减少代码的重复性,这点就不多说了。
看下面的例子:
public class Father { public Collection doSomething(HashMap map){ System.out.println("父类被执行..."); return map.values(); } }
public class Son extends Father { public Collection doSomething(Map map){ System.out.println("子类被执行..."); return map.values(); } }子类继承父类,并且重载了父类方法doSomething(),注意输入的参数,在父类中参数类型是HashMap,在子类中参数类型是Map,由Map要比HashMap的范围大。子类重载父类的方法后,就相当于子类有了两个方法,这两个方法的关系是重载的关系,即方法名相同,只有输入参数不同。这样一来,在使用到父类对象的地方,输入的参数范围肯定是较小的,比如这里的HashMap,而子类通过重载父类的方法,有了两种处理不同参数范围的方法,这样,当使用到父类的地方替换成子类后,由于传入的参数,是较小的范围的那个,所以会相应的调用从父类继承过来的方法。这样,把父类替换成子类后,运行结果不受到影响。这就是里氏替换原则想要表达的关键地方。如下面的测试代码:
public class Client { public static void main(String[] args) { //Father f=new Father(); Son f=new Son(); HashMap map=new HashMap(); f.doSomething(map); } }用子类替换父类后,程序运行结果是不变的。
假如说,子类的输入范围变小了,那么会是什么样的呢?来看代码:
public class Father { public Collection doSomething(Map map){ System.out.println("父类被执行..."); return map.values(); } }
public class Son extends Father { public Collection doSomething(HashMap map){ System.out.println("子类被执行..."); return map.values(); } }
public class Client { public static void main(String[] args) { //Father f=new Father(); Son f=new Son(); HashMap map=new HashMap(); f.doSomething(map); } }在子类替换父类后,由于输入的参数是HashMap类型的,那么会调用子类中自己定义的重载方法,这样结果就会改变了。但是这种情况不是绝对的,假如说输入的参数,本来就是较大范围的,如下:
public class Client { public static void main(String[] args) { //Father f=new Father(); Son f=new Son(); Map map=new HashMap(); f.doSomething(map); } }那么在子类替换掉父类后,其运行结果仍然没有变化。
综上:子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或更宽松。
这个要求可以很容易理解,在使用父类的地方,有一个较大的返回值,在父类被替换成子类后,若要是结果不受影响,那么子类返回的类型值,必须是小于或等于父类的返回值,即子类返回的值是父类返回值得子类。要不然,在程序中用到的都是父类返回的值,而替换成子类后,返回值如果比现在的值得范围还要大的话,是没法兼容的。
小结:
我觉得在面向对象中这种父子继承关系和我们生活中的这种父子继承关系有很大不同。在生活中,给我们的感觉是父要比子有本事,至少当子还是小孩更是如此,可是在程序中,子类永远比父类本事大,因为它不仅有父类的功能,还发展自己特殊的功能。
原文链接:https://www.f2er.com/javaschema/286803.html