里氏替换原则(Liskov Substitution Principle, LSP)
以此回顾所读《设计模式之禅》与《高校程序员的45个习惯》中Liskov部分
定义:
第一种:If for each object O1 of type S there is an object O2 fo type T such that for all programs P defined in terms of T, the behavior of P is unchanged when O1 is substitueted for O2 then S is a subtype of T. (对于每一个S类型的对象O1, 都有一个T类型的对象O2,使以T定义的程序P在使用O2替换O1时,行为不发生变化,则S是T的子类)。
第二种:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. (所有引用基类的地方都必须能够使用子类对象,而且使用者不用知道任何差异,不必自己进行任何修改)。
两种定义通俗地讲,就是只要能使用父类对象的地方,就可以使用子类对象,并且这样的替换不会给程序带来任何错误或异常,这是满足“里氏替换原则”的继承。(注意顺序,父类可以用子类来替换,但是子类不一定能被父类替换)
为什么使用里氏替换原则?
(1)如果不保证子类能透明替换父类,程序运行结果可能出现问题
假设某个类中有一个简单的方法,用来对字符串列表进行排序,然后返回一个新的列表。并如下调用:
utils = new BasicUtils();
...
sortedList = utils.sort(aList);
现在假定开发人员派生了一个BasicUtils的子类,并写了一个新的sort()方法,实现了更快更好的排序算法:
utils = new FasterUtils();
...
sortedList = utils.sort(aList);
注意对sort()的调用是完全一样的,一个FasterUtils对象完美地替换了一个BasicUtils对象,用utils.sort()代码可以处理任何类型的utils对象,而且可以正常工作。
但是如果开发人员派生了一个BasicUtils的子类,并改变了排序的意义,使返回的列表逆序排列,这样子类对象替换父类对象后,程序运行结果发生错误,违反了里氏替换原则。
(2)如果子类替换父类时需要增加额外的判断,即 使用者不能在不了解子类的情况下将父类替换为子类,则将增加维护难度。
试想,每增加一个子类,所有与父类有关的类都必须增加一个判断,显然不合理。例如,代码中在要用某个子类替换父类时使用了if……else,则开发人员必须要了解每个子类,才能进行编码维护,无疑不可取。
if (a instanceof b) {
//do something
} else {
//do something
}
因此在设计类的继承层次时,一定要考虑是否能够在不了解子类的情况下自由地替换父类。
(3)里氏替换原则可以增强程序的健壮性,版本升级也可以具有很好的兼容性,即使增加子类,原有的子类还是可以继续运行
如何理解和使用里氏替换原则?
1、子类必须完全实现父类的方法。
对于父类中定义的所有方法,在子类里都能不发生改变地实现,例如,步枪、冲锋枪、手枪都是继承自枪类,有一个共同的方法shoot(),如果添加一个新子类玩具枪,而玩具枪不能射击,所以它的shoot()方法中就do nothing,这样当你把枪这个类给士兵,期望他杀敌时,就需要判断给他的枪是玩具枪还是真枪,也就是说,由于玩具枪这个子类没有完整的实现父类的方法,或者说在实现父类的方法时发生了“畸变”,给代码维护带来了困难,违背了里式替换原则,此时建议断开继承,采用依赖、聚集、组合等关系代替继承。
2、子类可以有自己的个性
子类可以有自己不同于父类的属性和行为,里氏替换原则只强调子类可以自由替换父类,并不需要反过来也成立,而且往往用父类替换子类是不安全的。
3、子类中方法的前置条件(传入参数)必须与父类中被覆写的方法的前置条件相同或更宽松
先明白两个概念,覆写和重载,覆写是指方法名和传入参数完全相同,重载是指方法名相同,但传入参数不同。看一个例子:
父类:
public class Father {
public Collection doSomething(HashMap hashMap){
System.out.println("父类被执行。。。");
return hashMap.values();
}
}
子类:
public class Son extends Father {
//子类重载父类方法,放大参数范围
public Collection doSomething(Map map) {
System.out.println("子类被执行。。。");
return map.values();
}
}
场景类:
public class Client {
public static void invoker() {
Father father = new Father();
HashMap hashMap = new HashMap();
father.doSomething(hashMap);
}
public static void main(String[] args) {
invoker();
}
}
运行的结果:父类被执行。。。
根据里氏替换原则,如果替换父类为子类,即
Son son = new Son();
HashMap hashMap = new HashMap();
son.doSomething(hashMap);
运行的结果:父类被执行。。。
这个结果是正确的,子类的参数范围被放大后,替换父类所得的结果与调用父类的结果相同。但是,如果子类的参数范围小于父类的参数范围会怎样呢?
新父类:
public class Father {
public Collection doSomething(Map map){
System.out.println("父类被执行。。。");
return map.values();
}
}
新子类:
public class Son extends Father {
//子类重载父类方法,放大参数范围
public Collection doSomething(HashMap hashMap) {
System.out.println("子类被执行。。。");
return hashMap.values();
}
}
新场景类:
public class Client {
public static void invoker() {
Father father = new Father();
HashMap hashMap = new HashMap();
father.doSomething(hashMap);
}
public static void main(String[] args) {
invoker();
}
}
运行结果:父类被执行。。。
用子类替换父类:
Son son = new Son();
HashMap hashMap = new HashMap();
son.doSomething(hashMap);
运行结果:子类被执行。。。
运行结果出现了错误!子类在没有覆写父类方法的前提下,被执行了,这就会带来逻辑混乱,所以,子类方法中的前置条件必须与父类相同或比父类宽松。
4、覆写或实现父类的方法时输出结果可以被缩小
如果父类的一个方法的返回类型是T,子类重载或覆写的方法返回类型是S,里氏替换原则要求S必须小于等于T。
如果是覆写,父类和子类传入的参数是相同的,两个方法的返回值S<=T,这是覆写的要求。
如果是重载,里氏替换原则要求子类传入参数范围小于等于父类传入参数范围,所以子类方法是不会被调用的。也可以参考上面的前置条件。
- 基础篇章:关于 React Native 之 DrawerLayoutAndroid 组件的讲解
- Android 使用 ApachePOI 组件读写 Word doc 和 docx 文件
- Android 开发中如何动态加载 so 库文件
- 良心推荐:总结 Android 开发中必备的代码 Review 清单
- 强烈推荐:基于Java反射实现一个 Android ORM 框架
- Android 高效安全的本地广播 LocalBroadcast 完全解析
- Android的编译打包流程详解
- Android 仿京东、拼多多商品分类页
- 简单高效的实现 Android App 全局字体替换
- 基于 RxJava2+Retrofit2 精心打造的 Android 基础框架 XSnow
- Android 图片选择到裁剪之步步深坑
- Android自定义 View 实战之 StickerView
- 十分钟搞定酷炫动画,Android自定义 View 入门
- 基础篇章:关于 React Native 之 Switch 和 ProgressBarAndroid 组件的讲解
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 配置Tomcat使用https协议(单向认证)
- python五十八课——正则表达式(切割)
- python五十九课——正则表达式的拓展内容
- 配置SVN的hooks功能自动更新代码
- python六十课——高阶函数之map
- python六十一课——高阶函数之reduce
- python六十二课——高阶函数之filter
- Nginx优化配置详解
- python六十三课——高阶函数之sorted
- python六十四课——高阶函数练习题(一)
- python六十四课——高阶函数练习题(二)
- python六十四课——高阶函数练习题(三)
- Jenkins+Maven+Svn实现代码自动打包与发布
- python六十五课——单元测试(一)
- python六十六课——单元测试(二)