👉文章示例代码👈

定义

任何基类可以出现的地方,子类一定可以出现。

里氏替换原则是继承复用的基石,只有当子类可以替换掉基类且软件单位的功能不受到影响的时候,基类才能真正被复用,而子类也能够在基类的基础上去增加新的行为。

里氏替换原则通俗点来说就是:子类可以扩展父类的功能,但不能改变父类原有的功能

通过以上这句话,可以引申出以下几点含义:

  • 含义一:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 含义二:子类中可以增加自己特有的方法。
  • 含义三:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入、入参)要比父类方法的输入参数更宽松。
  • 含义四:当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。

如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系(可以采用依赖、聚集、组合等关系)。

含义讲解

在讲解开闭原则时,笔者通过扩展子类来覆写父类方法,同时子类提供额外方法来实现需求改动。👉点击跳转

代码粘贴至此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author zhh
* @description 食品打折类
* @date 2020-02-05 00:27
*/
public class FoodDiscount extends Food {

public FoodDiscount(Integer id, String name, Double price) {
super(id, name, price);
}

public Double getOriginPrice() {
return super.getPrice();
}

@Override
public Double getPrice() {
return this.getOriginPrice() * 0.6;
}
}

含义一讲解

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

上段代码中 getPrice() 其实已经对于原来的 Food 类中的 getPrice() 含义发生了变化,对其进行了打折。但这并不符合里氏替换原则。所以这里我们将打折的方法放置到新增的方法中,调整后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author zhh
* @description 食品打折类
* @date 2020-02-05 00:27
*/
public class FoodDiscount extends Food {

public FoodDiscount(Integer id, String name, Double price) {
super(id, name, price);
}

public Double getDiscountPrice() {
return super.getPrice() * 0.6;
}
}

这样调整后,我们可以看到子类并没有覆盖父类的非抽象方法 getPrice()

含义二讲解

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

含义一讲解的代码片段中 getDiscountPrice() 是子类新增的特有方法,其父类中并不存在该方法。

含义三讲解

当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。

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
/**
* @author zhh
* @description 父类
* @date 2020-02-07 16:50
*/
public class Father {

public void method(HashMap hashMap) {
System.out.println("父类被执行");
}
}

/**
* @author zhh
* @description 子类
* @date 2020-02-07 16:51
*/
public class Child extends Father {

// 重载方法
public void method(Map map) {
System.out.println("子类Map被执行");
}
}

/**
* @author zhh
* @description 测试类
* @date 2020-02-07 16:53
*/
public class Test {

public static void main(String[] args) {
Child child = new Child();
HashMap hashMap = new HashMap();
child.method(hashMap);
}
}

测试类的输出结果如下:

父类被执行

这个输出结构是正确的。

父类的方法入参是 HashMap 类型,而子类的方法入参是 Map 类型,子类的方法入参类型范围比父类大,那么子类的方法永远也不会被执行。

那我们反过来试下,调整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Father {

public void method(Map map) {
System.out.println("父类被执行");
}
}

public class Child extends Father {

// 重载方法
public void method(HashMap hashMap) {
System.out.println("子类HashMap被执行");
}
}

public class Test {

public static void main(String[] args) {
Child child = new Child();
HashMap hashMap = new HashMap();
child.method(hashMap);
}
}

测试类的输出结果如下:

子类HashMap被执行

可以看到这个时候程序执行了子类的 method 方法,这样就违反了里氏替换原则,在实际的开发过程当中很容易引起业务逻辑的混乱。

含义四讲解

当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。

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
/**
* @author zhh
* @description 父类
* @date 2020-02-07 17:11
*/
public abstract class Father {

public abstract Map method();
}

/**
* @author zhh
* @description 子类
* @date 2020-02-08 11:13
*/
public class Child extends Father {

@Override
public HashMap method() {
HashMap hashMap = new HashMap();
System.out.println("子类method方法被执行");
hashMap.put("username", "zhaohaihao");
return hashMap;
}
}

/**
* @author zhh
* @description 测试类
* @date 2020-02-08 11:17
*/
public class Test {

public static void main(String[] args) {
Child child = new Child();
System.out.println(child.method());
}
}

测试类的输出结果如下:

子类method方法被执行
{username=zhaohaihao}

也就是说在实现父类的抽象方法时,子类的方法返回值范围一定要小于父类方法的返回值范围。

那我们反过来试下,我们将子类方法的返回值改为 Object ,而父类方法的返回值仍保持 Map 不变。

image.png

Object 是所有类的基类,因此它的范围是大于 Map 的,这个时候编辑器已经给出了错误提示,如上图。

继承的缺点

  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
  2. 降低了代码的灵活性
  3. 增强了耦合性。当父类的常量、变量或者方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构

优点

  1. 约束继承泛滥(开闭原则的一种体现)
  2. 加强程序健壮性,在程序变更的同时可以做到良好的兼容性
  3. 提高代码的重用性
  4. 提高代码的可扩展性

里氏替换原则反应了基类与子类之间的关系,同时也是对开闭原则的补充以及对实现抽象化的具体步骤的规范。

参考