附链
你也可以在这些平台阅读本文:
定义
任何基类可以出现的地方,子类一定可以出现。
里氏替换原则是继承复用的基石,只有当子类可以替换掉基类且软件单位的功能不受到影响的时候,基类才能真正被复用,而子类也能够在基类的基础上去增加新的行为。
里氏替换原则通俗点来说就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
通过以上这句话,可以引申出以下几点含义:
- **含义一:**子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- **含义二:**子类中可以增加自己特有的方法。
- **含义三:**当子类的方法重载父类的方法时,方法的前置条件(即方法的输入、入参)要比父类方法的输入参数更宽松。
- **含义四:**当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系(可以采用依赖、聚集、组合等关系)。
含义讲解
在讲解开闭原则时,笔者通过扩展子类来覆写父类方法,同时子类提供额外方法来实现需求改动。👉点击跳转
代码粘贴至此:
/**
* @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()
含义发生了变化,对其进行了打折。但这并不符合里氏替换原则。所以这里我们将打折的方法放置到新增的方法中,调整后的代码如下:
/**
* @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()
是子类新增的特有方法,其父类中并不存在该方法。
含义三讲解
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
/**
* @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
类型,子类的方法入参类型范围比父类大,那么子类的方法永远也不会被执行。
那我们反过来试下,调整代码如下。
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
方法,这样就违反了里氏替换原则,在实际的开发过程当中很容易引起业务逻辑的混乱。
含义四讲解
当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。
/**
* @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
不变。
Object
是所有类的基类,因此它的范围是大于 Map
的,这个时候编辑器已经给出了错误提示,如上图。
继承的缺点
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
- 降低了代码的灵活性
- 增强了耦合性。当父类的常量、变量或者方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构
优点
- 约束继承泛滥(开闭原则的一种体现)
- 加强程序健壮性,在程序变更的同时可以做到良好的兼容性
- 提高代码的重用性
- 提高代码的可扩展性
里氏替换原则反应了基类与子类之间的关系,同时也是对开闭原则的补充以及对实现抽象化的具体步骤的规范。
参考
- 《Head First 设计模式》
- 《大话设计模式》
- 设计模式之里氏替换原则