里氏替换原则:
面向对象有三大特性:封装、继承、多态。所以我们在实际开发过程中,子类在继承父类后,根据多态的特性,可能是图一时方便,经常任意重写父类的方法,那么这种方式会大大增加代码出问题的几率。比如下面场景:类C实现了某项功能F1。现在需要对功能F1作修改扩展,将功能F1扩展为F,其中F由原有的功能F1和新功能F2组成。新功能F由类C的子类C1来完成,则子类C1在完成功能F的同时,有可能会导致类C的原功能F1发生故障。这时候里氏替换原则就闪亮登场了。
四层含义:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。子类可以实现父类的抽象方法很好理解,事实上,子类也必须完全实现父类的抽象方法,哪怕写一个空方法,否则会编译报错。
里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
在面向对象的设计思想中,继承这一特性为系统的设计带来了极大的便利性,但是由之而来的也潜在着一些风险。就像开篇所提到的那一场景一样,对于那种情况最好遵循里氏替换原则,类C1继承类C时,可以添加新方法完成新增功能,尽量不要重写父类C的方法。否则可能带来难以预料的风险,比如下面一个简单的例子还原开篇的场景:
public class C { public int fun1(int a, int b){ return a+b; } } public class C1 extends C{ @Override public int fun1(int a, int b) { return a-b; } } public class Client{ public static void main(String[] args) { C c = new C1(); System.out.println("2+1=" + c.fun1(2, 1)); } }
运行结果:2+1=1
上面的运行结果明显是错误的。类C1继承C,后来需要增加新功能,类C1并没有新写一个方法,而是直接重写了父类C的fun1方法,违背里氏替换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错。
子类中可以增加自己特有的方法
在做功能拓展的时候,子类尽量不要重写父类的非抽象方法。我们可以增加新的方法。
public class C1 extends C{ public int func2(int a, int b) { return a-b; } }
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
import java.util.HashMap; public class Father { public void func(HashMap m){ System.out.println("执行父类..."); } } import java.util.Map; public class Son extends Father{ public void func(Map m){//方法的形参比父类的更宽松 System.out.println("执行子类..."); } } import java.util.HashMap; public class Client{ public static void main(String[] args) { Father f = new Son();//引用基类的地方能透明地使用其子类的对象。 HashMap h = new HashMap(); f.func(h); } }
运行结果:执行父类...,然而如果互换了输入参数,如下所示:
import java.util.HashMap; public class Father { public void func(Map m){ System.out.println("执行父类..."); } } import java.util.Map; public class Son extends Father{ public void func(HashMap m){//方法的形参比父类的更宽松 System.out.println("执行子类..."); } } import java.util.HashMap; public class Client{ public static void main(String[] args) { Father f = new Son();//引用基类的地方能透明地使用其子类的对象。 HashMap h = new HashMap(); f.func(h); } }
运行结果:执行子类,此时,子类方法被执行了,这会引起业务逻辑混乱,因为在实际开发中,父类一般是抽象类,子类是实现类,传递这样一个实现类,就会歪曲了父类的意图。因此,子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。。
总结:
采用里氏替换原则的目的是增强程序的健壮性,版本升级也可以保持好的兼容性。即使增加子类