1.内容引入——继承体系的思考

在继承中,凡是在父类已经实现的方法,其实算是一种契约或者规范,子类不应该在进行更改(重写);但是,由于这一点不是强制要求,所以当子类进行重写的时候,就会对继承体系产生破坏。

同时,继承带来便利的时候,也有弊端:给程序带来了侵入性,增加了对象之间的耦合性,可移植性低。当你修改基类时,子类都需要进行相应的修改。

那么,如何能够保持继承的优点,同时减少缺点对程序的影响呢?也就是我们要讨论的主角——“里氏替换原则”。

2.里氏替换原则的定义

1.第一种定义

如果对每一个类型为S的对象o1, 都用类型为T的对象o2, 使得以T定义的所有程序P在所有的对象o1都替换为o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

2.第二种定义

所有引用基类的地方都必须能透明地使用其子类进行替换。

第二种类型通俗易懂,就是说,只要父类出现的地方,都应该能用其子类进行替换。 但是反过来却不一定成立,也就是子类所在的地方替换成父类,是不一定成立的。

根据定义,我们总结出以下几点:

1.子类必须实现父类定义的抽象方法。对于父类已经实现的非抽象方法,不应该进行重写。

结合代码理解一下:

public abstract class SuperClass {
public abstract void sayHi();
public void doSomething() {
System.out.println("父类被执行...");
} //定义一个加法运算函数
public int add(int i, int j) {
int result = i + j;
System.out.println("result = " + result);
return result;
}
} public class SubClass extends SuperClass {
@Override
public void sayHi() {
System.out.println("子类重写了父类的sayHi方法...");
} //子类特有的方法
public void selfMethod() {
System.out.println("子类特有的方法");
} //子类重写了父类的非抽象加法运算方法
@Override
public int add(int i, int j) {
int result = i - j;
System.out.println("result = " + result);
return result;
} } public class Client {
public static void main(String[] args) {
SuperClass clazz = new SubClass();
clazz.doSomething();
SubClass subClass = new SubClass();
//子类调用自己的方法
subClass.selfMethod();
SuperClass superClass = (SuperClass) subClass;
superClass.add(11,22);
}
} //运行结果如下:
父类被执行...
子类特有的方法
result = -11

当你继承一个基类时,编译器会要求你来实现基类的抽象方法,否则,会报错。

但是,对于已经实现的方法,编译器便不会强制让你去重写(也不推荐这样做):在上面的demo中,子类重写了父类的add方法,在调用时,出现了错误(基类定义的加法逻辑,被子类重写为了一个减法)。这样子,在父类出现的地方,不能由子类完全替换,违背了“里氏替换原则”。

2.子类可以有自己的方法。

在上面的demo中,子类定义了特有的方法selfMethod(),可以实现其他的业务逻辑。

   //子类特有的方法
public void selfMethod() {
System.out.println("子类特有的方法");
}

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

下来看一下demo:

public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行...");
return map.values();
}
} public class Son extends Father {
public Collection doSomething(Map map) {
System.out.println("子类被执行...");
return map.values();
}
} public class Client1 {
public static void main(String[] args) {
invoke();
} public static void invoke() {
// Father clazz = new Father();
Son clazz = new Son();
HashMap hashMap = new HashMap();
clazz.doSothing(hashMap);
} //运行结果:父类执行...

在demo中,基类定义了一个doSomething方法,接受的形参类型为HashMap,子类重载了基类的这个方法,并且将接受的形参类型扩展为Map。但是,我们发现,在父类出现的地方,替换为子类后,运行的结果不变。也就是说,子类对象在方法执行时被替换为父类对象,重载的方法并未被执行。如果想要执行子类的方法,就必须重写,这是正确的。

如果反过来呢?下面通过反证来证明第三点:

当子类的方法中的前置条件比父类小的时候,情况会怎么样呢?

在父类和子类中重新定义这两个方法,符合子类的前置条件小于父类这一条件即可。

   //修改父类的方法,前置条件扩大为Map
public Collection doSomething(Map map) {
System.out.println("父类被执行...");
return map.values();
} //修改子类,前置条件缩小为HashMap
public Collection doSomething(HashMap map) {
System.out.println("子类被执行...");
return map.values();
}

当客户端的invoke()方法中,通过Father类型调用doSomething方法时,运行结果如下:

//运行结果: 父类执行****

将对象换成Son类型时,执行结果时:

 public static void invoke() {
// Father clazz = new Father();
Son clazz = new Son();
HashMap hashMap = new HashMap();
clazz.doSothing(hashMap);
} //执行结果: 子类被执行...

我们看到了,子类并没有重写父类的方法,但是子类的方法被执行了(这是因为调用方法时,优先选择参数类型一致的方法,也就是重载方法,找不到的话才去找类型为形参的基类的方法),但是,这个现象在逻辑上面是错误的。

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

这一点是重写的要求之一,在这里就用代码展示了。

最佳实践

在前面的示例中,我们在子类SubClass中不小心重写了基类SupClass已经实现的方法add(),导致出现了逻辑错误。

1.在实际的使用中,我们要尽量避免重写父类已经实现的方法。

2.在适当的情况下减少使用继承,多使用聚合、组合、依赖等解决问题。

DesignPattern系列__04里氏替换原则的更多相关文章

  1. 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP

    前言 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle ). 英文原文:http ...

  2. 里氏替换原则(Liskov Substitution Principle)

    开放封闭原则(Open Closed Principle)是构建可维护性和可重用性代码的基础.它强调设计良好的代码可以不通过修改而扩展,新的功能通过添加新的代码来实现,而不需要更改已有的可工作的代码. ...

  3. 第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

    2. 里氏替换原则(Liskov Substitution Principle,LSP) 2.1 定义 (1)所有使用基类的地方必须能透明地使用子类替换,而程序的行为没有任何变化(不会产生运行结果错误 ...

  4. .net学习之继承、里氏替换原则LSP、虚方法、多态、抽象类、Equals方法、接口、装箱拆箱、字符串

    1.继承(1)创建子类对象的时候,在子类对象中会为子类对象的字段开辟空间,也会为父类的所有字段开辟空间,只不过父类私有的成员访问不到(2)子类从父类继承父类所有的非私有成员,但是父类的所有字段也会创建 ...

  5. 2.里氏替换原则(Liskov Substitution Principle)

    1.定义 里氏替换原则的定义有两种,据说是由麻省理工的一位姓里的女士所提出,因此以其名进行命名. 定义1:如果对一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1所定义的程序P中在o1全都 ...

  6. 【设计模式六大原则2】里氏替换原则(Liskov Substitution Principle)

      肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对 ...

  7. [OOD]违反里氏替换原则的解决方案

    关于OOD中的里氏替换原则,大家耳熟能祥了,不再展开,可以参考设计模式的六大设计原则之里氏替换原则.这里尝试讨论常常违反的两种形式和解决方案. 违反里氏替换原则的根源是对子类及父类关系不明确.我们在设 ...

  8. [设计模式]<<设计模式之禅>>关于里氏替换原则

    在面向对象的语言中,继承是必不可少的.非常优秀的语言机制,它有如下优点:● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性:● 提高代码的重用性:● 子类可以形似父类,但又异于父类,“龙 ...

  9. 设计模式值六大原则——里氏替换原则(LSP)

    里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题. 继承的优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性: 提高代码的重用性: 子类 ...

随机推荐

  1. 图片加载时间缓慢问题API

    一.背景       最近段时间,开发写值工具项目中,出现图片加载问题API,响应时间缓慢:为了优化图片加载问题,我进行图片压缩方法,然后API的图片加载还是慢,最终在自己无意中乱写找到了根本的原因. ...

  2. 基于STM32之UART串口通信协议(二)发送

    一.前言 1.简介 在上一篇UART详解中,已经有了关于UART的详细介绍了,也有关于如何使用STM32CubeMX来配置UART的操作了,而在该篇博客,主要会讲解一下如何实现UART串口的发送功能. ...

  3. RABC权限控制(页面操作角色,权限和进行分配)

    上一节主要说的是如何通过url进行权限控制,这一节就说一下如何开发一个权限控制的界面,这样我们就能很方便的创建角色,并分配给用户不同角色和不同权限. 1.编写角色管理页面 这个编写较为简单,主要是通过 ...

  4. HDU 6019:MG loves gold(暴力set)

    http://acm.hdu.edu.cn/showproblem.php?pid=6019 题意:给出n个颜色的物品,你每次取只能取连续的不同颜色的物品,问最少要取多少次. 思路:从头往后扫,用se ...

  5. 法国神器"mimikatz"简化版,一键导出结果

    神器之所以称之为神器.那是闹着玩的? 法国神器"mimikatz",那是相当的好使!!! GitHub:https://github.com/gentilkiwi/mimikatz ...

  6. 嵊州D2T1 “我只是来打个电话”

    嵊州D2T1 “我只是来打个电话” 精神病院有一个这样的测试. 给出一个正整数集合,集合中的数各不相同,然后要求病人回答: 其中有多少个数,恰好等于集合中另外两个(不同的)数之和? 回答正确的人,即可 ...

  7. Node.js热部署代码,实现修改代码后自动重启服务方便实时调试

    写PHP等脚本语言的时候,已经习惯了修改完代码直接打开浏览器去查看最新的效果.而Node.js 只有在第一次引用时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,这种设计虽然有利于提高性能,却 ...

  8. jekyll搭建个人博客2

    目录 个性化 jekyll目录结构 修改个人信息 修改头像 修改背景颜色 关于头像的效果 图片问题 域名 个性化 jekyll目录结构 个性化就是要对文件内容作出修改,使得博客外观发生变化,在修改文件 ...

  9. Gin 框架 - 使用 logrus 进行日志记录

    目录 概述 日志格式 Logrus 使用 推荐阅读 概述 上篇文章分享了 Gin 框架的路由配置,这篇文章分享日志记录. 查了很多资料,Go 的日志记录用的最多的还是 github.com/sirup ...

  10. 【题解】P1396 营救-C++

    原题传送门 这道题目基本就是一个克鲁斯卡尔最小生成树的模板题,唯一不同的是,这道题目的最终目标不是所有点相连,而是只要s和t相连就可以了.还有就是这道题目求的是最小生成树中的最大边权值.但是,克鲁斯卡 ...