本文最后更新于:2021年12月13日 下午
里式替换原则
1、定义
里式替换原则(Liskov Substitution Principle,LSP)表示: 所有引用基类的地方必须能够透明地使用其子类的对象。
换句话说就是,父类出现的地方可以用当前类的某一个子类来替代,反之则不然。
2、优点
1. 减少继承带来的缺点, 增强程序的健壮性, 在后续的升级改造中可以保持更好的兼容性;
2. 提高代码的复用性, 共性可以抽象为父类方法;
3. 提高代码的扩展性,由于子类可以包含但不限于父类的功能, 因此子类可以任意添加属于自己专属的功能。
4. 里式替换原则是指导设计父子类的设计原则。
3、示例
在原先的继承设计上存在这么一个问题, 如果一个父类存在多个子类,并且这些子类还使用了父类的同一个方法,
那么在对父类的方法进行修改时则需要同时考虑会不会影响子类,比如下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package cc.niushuai.study.designpattern.sixprinciples.lsp;
class Father {
public void print() { System.out.println("i am father"); } }
class Son extends Father { @Override public void print() { System.out.println("i am son"); } }
class Daughter extends Father { @Override public void print() { System.out.println("i am daughter"); } }
public class Main {
public static void main(String[] args) {
Father father = new Father(); father.print();
Son son = new Son(); son.print();
Daughter daughter = new Daughter(); daughter.print(); } }
|
运行结果:
1 2 3
| i am father i am son i am daughter
|
此处由于子类已经对父类的方法进行了覆写,因此子类出现的地方不可以替换为父类, 否则得到的结果就是一个子类输出的结果, 与父类无关。
4、里式替换原则的四个原则
1、子类必须实现父类的抽象方法,但不得重写父类的非抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package cc.niushuai.study.designpattern.sixprinciples.lsp.main2;
class Father { public void run() { System.out.println("father run..."); } }
class Son extends Father { @Override public void run() { System.out.println("son run..."); } }
public class Main2 {
public static void main(String[] args) {
System.out.println("父类运行结果..."); Father father = new Father(); father.run();
System.out.println("子类运行结果..."); Son son = new Son(); son.run(); } }
|
运行结果:
1 2 3 4
| 父类运行结果... father run... 子类运行结果... son run...
|
子类继承父类重写了run
方法,然而子类的fly
方法已经和父类不一致了,得到的结果与父类不同,
因此此处不能使用子类替换父类,违背了里式替换原则。
2、子类可以有自己的方法
子类源于父类, 因此具有父类的一切能具有的东西, 并且还可以扩展出属于自己的东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package cc.niushuai.study.designpattern.sixprinciples.lsp.main2;
class Father { public void run() { System.out.println("father run..."); } }
class Son extends Father {
public void run2() { System.out.println("son run 2..."); } }
public class Main2 {
public static void main(String[] args) {
System.out.println("父类运行结果..."); Father father = new Father(); father.run();
System.out.println("子类运行结果..."); Son son = new Son(); son.run();
System.out.println("子类调用自己的方法结果..."); son.run2(); } }
|
运行结果:
1 2 3 4 5 6 7 8 9
| 父类运行结果... father run... 子类运行结果... father run... 子类调用自己的方法结果... son run 2...
Process finished with exit code 0
|
可以看到 son.run()
与 father.run()
得到的结果一样, 并且 son.run2()
存在自己的处理逻辑, 符合里式替换原则。
3、当子类覆盖或实现父类的方法时,方法的输入参数可以比父类方法的输入参数更宽松
父类的方法形参类型为T
,子类的形参为S
,那么要求S>T
举例: 父类的方法形参为HashMap<String, Object>
子类的方法形参为Map<String, Object>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package cc.niushuai.study.designpattern.sixprinciples.lsp.main3;
import java.util.HashMap; import java.util.Map;
class Father {
public void run(HashMap<String, Object> hashMap) { System.out.println("父类 run..."); } }
class Son extends Father {
public void run(Map<String, Object> map) { System.out.println("子类 run..."); } }
public class Main3 {
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
System.out.println("父类结果"); Father father = new Father(); father.run(map);
System.out.println("子类结果"); Son son = new Son(); son.run(map); } }
|
运行结果:
1 2 3 4
| 父类结果 父类 run... 子类结果 父类 run...
|
我们可以看到子类的形参范围大于父类时, 并没影响到结果的输出 符合里式替换原则
4、当子类覆盖或实现父类的方法时,方法的返回结果可以比父类方法的返回结果范围更严格
父类的方法形参类型为T
,子类的形参为S
,那么要求S<T
举例: 父类的方法形参为Map<String, Object>
子类的方法形参为HashMap<String, Object>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package cc.niushuai.study.designpattern.sixprinciples.lsp.main4;
import java.util.HashMap; import java.util.Map;
class Father { public Map<String, Object> run() {
System.out.println("父类 run ..."); return new HashMap(); } }
class Son extends Father { @Override public HashMap<String, Object> run() {
System.out.println("子类 run ..."); return new HashMap<>(); } }
public class Main4 {
public static void main(String[] args) {
System.out.println("父类结果"); Father father = new Son(); father.run();
System.out.println("子类结果"); Son son = new Son(); son.run(); } }
|
运行结果:
1 2 3 4
| 父类结果 子类 run ... 子类结果 子类 run ...
|
如果子类的方法形参为Map<String, Object>
父类的方法形参为HashMap<String, Object>
是无法编译通过的,
因此这种方式的使用,子类覆写了则会调用子类的方法, 前提是父类的引用指向子类
里式替换原则 可能更偏向于子类不要刻意修改父类的实现, 可以存在自己的实现。