里氏代换原则(Liskov Substitution Principle、LSP)

版权声明: https://blog.csdn.net/qq_42499188/article/details/84974826

一、概念

氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  简单的理解为一个软件实体如果使用的是一个父类,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,软件里面,把父类都替换成它的子类,程序的行为没有变化。

  子类型必须能够替换掉它们的父类型。

二、继承的优缺点

优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类,但是又异于父类。
  • 提高代码的可扩展性,实现父类的方法就可以了。许多开源框架的扩展接口都是通过继承父类来完成。
  • 提高产品或项目的开放性

缺点:

  • 继承是侵入性的,只要继承,就必须拥有父类的所有方法和属性
  • 降低了代码的灵活性,子类必须拥有父类的属性和方法,让子类有了一些约束
  • 增加了耦合性,当父类的常量,变量和方法被修改了,需要考虑子类的修改,这种修改可能带来非常糟糕的结果,要重构大量的代码

三、四层含义

里氏替换原则包含以下4层含义:

  • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

 现在我们可以对以上四层含义逐个讲解。

子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

  在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。子类可以实现父类的抽象方法很好理解,事实上,子类也必须完全实现父类的抽象方法,哪怕写一个空方法,否则会编译报错。

  里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

  在面向对象的设计思想中,继承这一特性为系统的设计带来了极大的便利性,但是由之而来的也潜在着一些风险。就像开篇所提到的那一场景一样,对于那种情况最好遵循里氏替换原则,类C1继承类C时,可以添加新方法完成新增功能,尽量不要重写父类C的方法。否则可能带来难以预料的风险,比如下面一个简单的例子的场景:

public class C {

    public int func(int a, int b){

        return a+b;

    }

}



public class C1 extends C{

    @Override

    public int func(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.func(2, 1));

    }

}

// 运行结果:2+1=1

  上面的运行结果明显是错误的。类C1继承C,后来需要增加新功能,类C1并没有新写一个方法,而是直接重写了父类C的func方法,违背里氏替换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错。

子类中可以增加自己特有的方法

  在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其符合里氏替换原则,代码如下:

public class C {
    public int func(int a, int b){
        return a+b;
    }
}
 
public class C1 extends C{
    public int func2(int a, int b) {
        return a-b;
    }
}
 
public class Client{
    public static void main(String[] args) {
        C1 c = new C1();
        System.out.println("2-1=" + c.func2(2, 1));
    }
}
//运行结果:2-1=1

当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

代码示例

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);
    }
}
// 运行结果:执行父类...

注意Son类的func方法前面是不能加@Override注解的,因为否则会编译提示报错,因为这并不是重写(Override),而是重载(Overload),因为方法的输入参数不同。

当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

代码示例:

import java.util.Map;
public abstract class Father {
    public abstract Map func();
}
 
import java.util.HashMap;
public class Son extends Father{
     
    @Override
    public HashMap func(){//方法的返回值比父类的更严格
        HashMap h = new HashMap();
        h.put("h", "执行子类...");
        return h;
    }
}
 
public class Client{
    public static void main(String[] args) {
        Father f = new Son();//引用基类的地方能透明地使用其子类的对象。
        System.out.println(f.func());
    }
}
// 执行结果:{h=执行子类...}

猜你喜欢

转载自blog.csdn.net/qq_42499188/article/details/84974826