定义:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。这种描述不太好理解,里氏替换原则还有第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。
场景:有一功能F1(会飞),由类A完成。现需要将功能F1进行扩展,扩展后的功能为F(会飞、会游泳),其中F由原有功能F1(会飞)与新功能F2(会游泳)组成。新功能F(会飞、会游泳)由类A的子类B(继承A)来完成,则子类B在完成新功能F2(会游泳)的同时,有可能会导致原有功能F1(会飞)发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能F2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,子类可以扩展父类的功能,但不能改变父类原有的功能。说了那么多,其实最终总结就两个字:抽象。
下面通过上面的场景来举个例子,用来暴露不遵循里氏替换原则可能会出现的问题:
1.定义一个Animal类,此类有个功能fly(会飞):
/**
* Created by LJW on 2018/8/23.
*/
public class Animal {
public void fly(){
}
}
2.定义一个Fish类,此类继承于Animal类,并扩展新的功能swim(会游泳):
/**
* Created by LJW on 2018/8/23.
*/
public class Fish extends Animal {
String name;
public Fish(String name){
this.name = name;
}
@Override
public void fly() {
Log.i(name, "远走高飞啦~-~");
}
public void swim(){
Log.i(name, "遨游大海啦~-~");
}
}
3.初始化Fish类,实现里面的功能:
Fish fish = new Fish("小黄鱼");
fish.fly();
fish.swim();
最终的打印结果为:
小黄鱼: 远走高飞啦~-~
小黄鱼: 遨游大海啦~-~
惊讶吧!原本只会游泳的小黄鱼,继承Animal类之后竟然会飞了!根本原因是,子类覆盖了父类的非抽象方法。这里,为了遵循里氏替换原则,我们可以修改Fish类中的代码,不让其覆盖fly()方法。
说明:这里我只是为了暴露出不遵循里氏替换原则可能会出现的问题,所以不要去纠结程序本身的调用!
在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,重写父类的抽象方法,但不能覆盖父类的非抽象方法。
里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。