首页 > 代码库 > 里氏替换原则

里氏替换原则

里氏替换原则:

      面向对象有三大特性:封装、继承、多态。所以我们在实际开发过程中,子类在继承父类后,根据多态的特性,可能是图一时方便,经常任意重写父类的方法,那么这种方式会大大增加代码出问题的几率。比如下面场景:类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));
    }
}
View Code

运行结果: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);
    }
}
View Code

运行结果:执行父类...,然而如果互换了输入参数,如下所示:

技术分享
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);
    }
}
View Code

运行结果:执行子类,此时,子类方法被执行了,这会引起业务逻辑混乱,因为在实际开发中,父类一般是抽象类,子类是实现类,传递这样一个实现类,就会歪曲了父类的意图。因此,子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。。

 

总结:

      采用里氏替换原则的目的是增强程序的健壮性,版本升级也可以保持好的兼容性。即使增加子类

里氏替换原则