参考书籍:设计模式之禅 --- 秦小波 著
参考文章:JAVA中,多态是不是违背了里氏替换原则?? - aaron hao的回答 - 知乎https://www.zhihu.com/question/27191817/answer/145013324
第一种定义:如果对每一个类型为S的对象o1,都有类型为T 的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2,时,程序P的行为没有任何变化,那么类型S是类型T的子类型。
第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。
简单来讲就是:
只要父类能出现的地方子类就能出现,并且将父类替换为子类时不会产生任何错误或异常。反过来就不行,有子类出现的地方,父类不一定能适应。
里氏替换原则的目的:规范继承时子类的书写规则,保持父类方法不被覆盖
从上面可以看出以下几点
里氏替换原则是针对继承而言的,
①若子类继承父类是为了实现代码重用,那么父类的方法不能被子类重写,也就是父类方法应该保持不变。
②若子类继承父类是为了实现多态,那么子类就会覆盖并重写父类的方法。在这个时候,我们应该将父类定义为抽象类,并定义抽象方法,这样做的目的是:抽象类是不能实例化的,因此不存在父类实例化的情况,也就不存在子类实例替换父类实例的情况。
总结:尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。这样就可以实现:对扩展(基于抽象)是开放的,对变更(基于具体)是禁止的
里氏替换原则包含了4层含义
①子类必须完全实现父类的抽象类方法,但不能覆盖父类的非抽象类方法
代码重现:
建立一个抽象类或接口
public abstract class AbstractPerson {
//定义一个生孩子的抽象方法
public abstract void getChild();
}
建立两个子类分别继承abstractPerson
public class Asian extends AbstractPerson {
@Override
public void getChild() {
System.out.println("生一个亚洲宝宝");
}
}
public class African extends AbstractPerson {
@Override
public void getChild() {
System.out.println("生一个非洲宝宝");
}
}
调用实现
public class Demo_01 {
//在类中调用其他类时,务必使用其父类或接口,若不能,说明违反LSP原则
private AbstractPerson abstractPerson;
public void setAbstractPerson(AbstractPerson abstractPerson) {
this.abstractPerson = abstractPerson;
}
public void pregnant(){
System.out.println("人怀孕了");
abstractPerson.getChild();
}
public static void main(String[] args) {
Demo_01 demo = new Demo_01();
//父类出现的地方,我任意new子类,都会正常运行
demo.setAbstractPerson(new Asian());
demo.pregnant();
}
}
在这个代码中,不管African还是Asian都完全的实现了父类abstractPerson的方法,并且我们不需要知道在调用pregnant方法的时候,究竟是哪种人,这个在 demo.setAbstractPerson(new Asian());时确定
假如现在有一个机器人robot类,显然机器人是不可能怀孕生宝宝的,若 机器人robot类继承abstractPerson类,那么就必然要实现pregnant方法,而这个方法在现实中是无法实现的,这样就不满足里氏替换原则。
换一种思路来讲,可以将这个机器人robot类看做一个抽象父类,脱离继承,然后跟AbstractPerson抽象类建立委托关系,可以将robot类的模样,性别等委托给AbstractPerson处理等,这样这两个抽象父类的子类自由延展,互不影响,这样就符合里氏替换原则。
②子类可以有自己的个性
在里氏替换原则中,任何基类或接口的子类都可以增加自己特有的方法。
代码重现
建立一个抽象类或接口
public abstract class AbstractPerson {
//定义一个生孩子的抽象方法
public abstract void getChild();
}
继承abstractPerson
public class Asian extends AbstractPerson {
@Override
public void getChild() {
System.out.println("生一个亚洲宝宝");
}
}
创建一个Chinese子类继承Asian
public class Chinese extends Asian {
public void speak(){
System.out.println("我们都是中国人");
}
public void race(){
System.out.println("我们都说中国话");7
}
@Override
public void getChild() {
System.out.println("生了一个中国宝宝");
}
public void havior(Chinese chinese){
speak();
race();
getChild();
}
}
测试类
public class Demo_01 {
public static void main(String[] args) {
Chinese aaa = new Chinese();
aaa.havior(new Chinese());
}
}
----------------------output-------------------------
我们都是中国人
我们都说中国话
生了一个中国宝宝
在 public void havior(Chinese chinese) 中传入的是Chinese,系统测试类传入的也是Asian子类 Chinese,若在这里我们传入的是父类Asian呢,可不可以经过向下转型通过呢
public class Demo_01 {
public static void main(String[] args) {
Chinese aaa = new Chinese();
aaa.havior((Chinese)new Asian());
}
}
----------------------output-------------------------
Exception in thread "main" java.lang.ClassCastException:
com.zengjie.homework.exercise.Asian cannot be cast to com.zengjie.homework.exercise.Chinese
at com.zengjie.homework.exercise.Demo_01.main(Demo_01.java:10)
结果输出时会报类型转换异常,向下转型是不安全的,同时子类出现的地方,父类未必可以出现。
③覆盖或实现父类的方法时,子类输入参数范围要相同或更宽松
建立一个父类,传入值为HashMap
public class Father {
public Collection getCollection(HashMap hashMap){
System.out.println("Father 被执行");
return hashMap.values();
}
}
建立一个子类,继承父类,传入值为Map
public class Son extends Father {
public Collection getCollection(Map map) {
System.out.println("Son 被执行");
return map.values();
}
}
测试类1
public class Demo_01 {
public static void main(String[] args) {
Father father = new Father(); //父类存在时
father.getCollection(new HashMap());
}
}
--------------------output-------------------
Father 被执行
测试类2
public class Demo_01 {
public static void main(String[] args) {
Son son = new Son();//子类存在时
son.getCollection(new HashMap());
}
}
--------------------output-------------------
Father 被执行
子类Son重载父类方法,子类的输入值范围比父类大。
在测试类1中调用父类时,传入值为HashMap,子类不会被调用,只有父类被调用。
在测试类2中调用子类,传入值为HashMap,子类还是不会被调用,只有父类被调用。
原因很简单,子类的输入值范围比父类的大
要是反过来呢,父类的输入值范围比子类的大,那么上述代码会不会出错呢?
建立一个父类,传入值为Map
public class Father {
public Collection getCollection(Map map){
System.out.println("Father 被执行");
return map.values();
}
}
建立一个子类,继承父类,传入值为HashMap
public class Son extends Father {
public Collection getCollection(HashMap hashMap) {
System.out.println("Son 被执行");
return hashMap.values();
}
}
测试类1
public class Demo_01 {
public static void main(String[] args) {
Father father = new Father(); //父类存在时
father.getCollection(new HashMap());
}
}
--------------------output-------------------
Father 被执行
测试类2
public class Demo_01 {
public static void main(String[] args) {
Son son = new Son();//子类存在时
son.getCollection(new HashMap());
}
}
--------------------output-------------------
son 被执行
子类Son重载父类方法,子类的输入值范围比父类小。
在测试类1中调用父类时,传入值为HashMap,子类不会被调用,只有父类被调用。
在测试类2中调用子类,传入值为HashMap,子类会被调用,父类不会被调用。
显然,这并不满足里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。
将父类替换为子类,程序会出现异常或错误。
④覆写或实现父类的方法时,子类输出结果(返回值)范围要比父类相同或更严格
如果是重写:父类与子类的同名方法的输入参数是相同的,父类的返回值范围要大于子类的返回值范围
如果是重载:父类与子类的同名方法的输入参数类型或数量是不相同的,子类的输入参数范围宽度要大于或等于父类的输入参数范围
里氏替换原则的目的:
网友评论