里氏替换原则的定义

  里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立

 

里氏替换原则的作用

  里氏替换原则是实现开闭原则的重要方式之一。

  它克服了继承中重写父类造成的可复用性变差的缺点,因为它推崇,子类可以新增方法,但是不要去覆盖父类已经写好的方法

  它是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误。降低了代码出错的可能性。因为里式替换原则保证了子类不要去覆盖父类方法,因此,新增新的子类时。父类定义的东西不会改变。也就是说,进行功能扩展时,原有的代码不会出错

 

 

里式替换原则的实现方法

  里式替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类已经实现的方法。

  如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是用多态用的比较频繁时,程序出错的概率就会变大。因为子类重写毕竟覆盖了父类的功能,当需要使用父类的功能时,就容易导致出错。而且,里氏替换和多态并不冲突,我们尽量在抽象类或者接口来做多态,简单父类一般只是用来自下而上的封装公有域。

  如果程序违背了里式替换原则,则继承类的对象在基类出现的地方会出现运行错误。因为子类重写了方法,那么原本为基类的地方就不一定能用子类来代替了。我们如何判断程序是否违背了里式替换呢。判断方法如下。判断子类是否能继承父类的方法,如果可以继承,则符合里氏替换原则。如果不能继承,则不符合。意思就是,在使用父类的地方把子类代进去,如果方法不会出错,则符合。

 

  下面以“几维鸟不是鸟”为例来说明里氏替换原则。

【例2】里氏替换原则在“几维鸟不是鸟”实例中的应用。

分析:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期,其类图如图 1 所示。

“几维鸟不是鸟”实例的类图
图1 “几维鸟不是鸟”实例的类图


程序代码如下:

  1. package principle;
  2. public class LSPtest
  3. {
  4. public static void main(String[] args)
  5. {
  6. Bird bird1=new Swallow();
  7. Bird bird2=new BrownKiwi();
  8. bird1.setSpeed(120);
  9. bird2.setSpeed(120);
  10. System.out.println("如果飞行300公里:");
  11. try
  12. {
  13. System.out.println("燕子将飞行"+bird1.getFlyTime(300)+"小时.");
  14. System.out.println("几维鸟将飞行"+bird2.getFlyTime(300)+"小时。");
  15. }
  16. catch(Exception err)
  17. {
  18. System.out.println("发生错误了!");
  19. }
  20. }
  21. }
  22. //鸟类
  23. class Bird
  24. {
  25. double flySpeed;
  26. public void setSpeed(double speed)
  27. {
  28. flySpeed=speed;
  29. }
  30. public double getFlyTime(double distance)
  31. {
  32. return(distance/flySpeed);
  33. }
  34. }
  35. //燕子类
  36. class Swallow extends Bird{}
  37. //几维鸟类
  38. class BrownKiwi extends Bird
  39. {
  40. public void setSpeed(double speed)
  41. {
  42. flySpeed=0;
  43. }
  44. }


程序的运行结果如下:

如果飞行300公里:
燕子将飞行2.5小时.
几维鸟将飞行Infinity小时。


程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。其类图如图 2 所示。

“几维鸟是动物”实例的类图
图2 “几维鸟是动物”实例的类图
 
  
 
 
里氏替换原则和多态之间的关系
  在第一眼看到里氏替换原则时,我就产生了困惑。为什么要去禁止子类重写父类方法呢。这不是java多态的基本吗,那里式替换原则是不是和多态概念有冲突呢。后来在和朋友交流时候慢慢的有了一些感悟。故而记录下来。
  首先在我们的java语言中。可以用来实现多态的方式有很多种。一般为普通父类、抽象类和接口。而对普通父类而言。它不要用来做多态。普通的父类是自下而上的封装共性域的一种形式。一般而言,我们封装普通父类,是因为有共有的属性、方法。不需要去重写,而是自下而上的封装共性。因此针对普通父类中定义的方法,其实就是子类的共性方法,原理上如果不是共性的方法,就不能抽象为父类。因此普通父类中定义且实现的方法,子类一概不要去重写。这就能看出来里式替换的原理。而多态一般是在抽象类或者接口层去做的。这是因为。抽象类和接口都是自上而下的定义行为约束。例如在抽象类或者接口中,定义的抽象方法,并无具体实现,这就是表明子类必须要去重写该方法来实现多态。而针对抽象类或者接口中已经定义且做了实现的方法,则就是共性定义的方法。继承抽象类或者实现接口的类都不应该去覆盖该方法,就直接拿来用就好了。这也是里氏替换原则的体现